Early Advance
An early advance lets a qualified user take a new float while their previous float’s repayment
is still in flight — the ACH debit submitted but not settled (ACHSENT). Normally any
non-COMPLETED float denies RuleGoodStanding; early advance
discounts that one in-processing float for users whose repayment evidence is strong.
Two qualification paths
Users split by float rank (completed-float count); the populations are disjoint, so exactly one path can apply to a user and there is no fallback between them. Both produce the same outcome — only how the user qualifies differs.
| Float rank | Path | What clears the user |
|---|---|---|
0 |
|
The repayment debit is visible in their bank transactions for the current day bucket: a
FloatMe debit for exactly float + fee, no return or reversal, no FloatMe credit undoing the
debit ( |
1–6 |
neither |
No early advance is offered. |
>= 7 |
|
Their record: float rank >= 7, last 3 billed subscriptions paid back to back, and at most 1.4
repayment attempts per float over their 3 most recent completed floats
( |
A rank >= 7 user excluded from the experiment, or one who fails a float-rank check, gets nothing — the outcome is recorded, not retried against the other path.
Structural gate
Both paths share one precondition, resolved by the rule runner (it needs float-service and
payments-service calls the rules cannot make): the user’s most recent float is ACHSENT with no
other outstanding float, its latest collection attempt submitted a debit (carries a payment
confirmation ID), and that payment is still an ACHSENT debit. The gate also reports which
collection run produced the debit and how many debits exist for the float.
Per-path collection requirements on top of the gate:
-
transactions— the run must beTOMORROW_FIRST_FLOAT(the T-1 day-before-ACH collection for a first float). -
float_rank— the run must beTOMORROW_RETURNING_FLOAT, and the collection must be on its first debit attempt (denies withcollection_retried). "First attempt" counts submitted debits (runs carrying a confirmation ID), not collection log entries — runs that submitted nothing still write logs.
The floats.collections.day_before.returning_float_ach operations flag is not read by either
path; the collection log is the record of what ran.
Experiment arms
Each path is gated by a string-valued GrowthBook feature that assigns an arm
(Feature Flags): early_advance_transactions_group for rank 0,
early_advance_float_rank_group for rank >= 7. The transactions path additionally sits behind
the underwriting.rules.early_advance_elligibility_checks kill switch.
| Value | Behavior |
|---|---|
|
Evaluated; a cleared user is granted the advance, the grant IDs are persisted, and the notification is sent. |
|
Evaluated identically and recorded, but never granted — the |
anything else |
Not in the experiment; nothing is evaluated or recorded. |
holdout vs treatment is the only valid comparison — unbucketed users are a mixed
population, not a control. The live check re-reads the arm, so reassigning a user mid-experiment
denies an already-granted advance at the point of use.
|
The float-rank path’s payments-service lookup (for the attempt ratio) is fetched only for users over the rank bar and in an arm; a failed lookup drops the user from the experiment rather than recording a denial.
Float-rank path checks
Paid-subscription run — the last 3 billed cycles must be COMPLETED back to back. Leading
future-dated SCHEDULED cycles are skipped; counting stops at the first non-COMPLETED cycle,
so a WAIVED or errored cycle breaks the run. [SCHEDULED, COMPLETED, WAIVED, COMPLETED,
COMPLETED] is a run of 1, not 3.
Repayment-attempt ratio — debit attempts divided by the user’s 3 most recent COMPLETED
floats; must be non-zero and at most 1.4 (one retry in the window passes, two do not; both
constants live in pkg/rules/rule_good_standing.go). A completed float with no debit on record
still counts in the denominator, so the ratio can be below 1 — a low value may be a clean run or
a payment-data gap; repayment_attempts and floats_considered are persisted to distinguish
them. A ratio of exactly zero denies (attempt_ratio_zero). The in-processing float is never in
the window. This measure is distinct from RuleCollectionsErrors' error_ratio (failed Usio
debits, whole history, includes subscriptions) — they are not interchangeable.
Where each path is evaluated
-
float_rankruns inside RuleGoodStanding (evaluateFloatRankEarlyAdvance) — the rule receives the structural gate onRuleExecutionDataand runs the checks itself. -
transactionsruns in the rule runner (rulerunner.resolveEarlyAdvance→qualifyEarlyAdvanceByTransactions), which records the verdict on the gate it hands RuleGoodStanding.
Rules stay pure functions of RuleExecutionData — the rule runner resolves the experiment arm
and hands it over on the gate (EarlyAdvanceGate.Group), so the recorded arm always matches the
arm that gated the data.
What is saved
| Where | What | Notes |
|---|---|---|
|
Arm, verdict, |
Written for both arms; the result runner sets |
EvaluationResult top-level grant keys |
|
Superseded by the record above but dual-written and still what the live check reads. Written only for a granted advance. |
payments-service |
|
Links eligibility and the taken advance to the collection. |
Never record a holdout user’s float/payment under the top-level grant keys — the live
check treats their presence as permission, which would hand the holdout arm a working advance.
Diagnostics belong in early_advance_experiment only.
|
Each stored evaluation also reports early_advance_qualification on the summary-history
responses (pkg/dynamo/evaluation_results.go, EarlyAdvancePath): the path that cleared the
user, null when no path did. It reads early_advance_experiment.granted OR the top-level ID
pair, defaulting to transactions for records that predate the qualification field. It records
the past decision; the live check below is the authority on current eligibility.
Live check at request time
The float-check and eligibility endpoints re-verify a stored grant along the same path that
granted it (early_advance_qualification):
-
Common: the payment must still be an
ACHSENTdebit, and the user still intreatment. -
transactions: re-confirms the user is still a first-time floater. While the current day bucket matches the recordedearly_advance_day(and the float is stillACHSENT), the stored decision is trusted without re-fetching — any new transaction batch triggers a full rule re-run. Once the bucket advances, the check re-fetches the collection payment by confirmation ID and re-runs the day checks over transactions since its submit date. -
float_rank: re-confirms the float is still the user’s most recentACHSENTfloat and reads the authoritative payments-service status (the float’s cachedDebitStatuslags settlement).
The day bucket is the number of business days (weekends and federal bank holidays skipped)
between the float’s due date and now, 0 = the due date. Days 0–2 share the same checks: a
floatme transaction whose amount equals float + fee, no return/reversal transaction, and
no reversing floatme credit. A day with no entry denies. Descriptors are normalized
(lowercased, non-alphanumeric stripped) and amounts compared in cents. A stale recorded decision
self-corrects when the bucket advances — there is no cleanup job.
The result is surfaced as early_advance_eligible on the float-check response and
float_stacking_eligible on the eligibility-check response (name retained for app
compatibility), with early_advance_float_id/early_advance_payment_id included when eligible.
Notification
The rule runner builds the early_advance_eligible payload (email, collection-payment metric
fields) and carries it on user_rule_run_completed; the result runner decides whether to
send, from float_results.approved and early_advance_experiment.granted — carrying a payload
is not permission to send, which is what keeps holdout users from being nudged. Sent via
Iterable with user_id, float_id, day, and end_date (the business day the bucket rolls
over). Failures are logged and never fail the result run.
Decision metric
underwriting.early_advance.decision (Datadog counter), emitted for both arms — filter on
group before reading approval rates. Tags:
| Tag | Values |
|---|---|
|
|
|
Denying check: transaction day checks ( |
|
|
|
|
|
Day bucket |
|
Bank display name (persisted on the evaluation) |
|
|
payments.ach.early_advance_float tracks event:user_notified and event:float_created
(tagged payment_amount, payment_processor, business_days, process — not
qualification). Every float-rank decision also logs
float rank early advance <arm> - <verdict> with all check inputs.
Source: pkg/float_evaluation/early_advance.go, pkg/datadog/metrics.go