Float Lifecycle

A float is a short-term advance disbursed to a user. This page is the canonical reference for float statuses and every lifecycle operation outside automated collection: creation, manual payback, reactivation payback, requirements bypass, user bans, and terminal states. Automated collection is covered in Collections Engine.

Float Statuses

The ach_debit_status column on the float record tracks where a float is in its lifecycle.

Status Description Set by

SCHEDULING

Float created and disbursement submitted; awaiting its due date.

Float creation (API)

ACHSENT

ACH debit submitted to the payment processor; awaiting a settlement callback from the prod-payments Kinesis stream.

ACH-submitting collection runs; manual payback (ACH option)

COMPLETED

Fully collected. Terminal.

ACH Handler (settlement); any successful pinless debit

RETRY

A collection attempt failed; the float is overdue and targeted by retry collection.

Due-date and retry runs; webhook worker; ACH Handler (non-bannable return)

FAILED

A float operation errored; treated as overdue.

batch-worker

ACHFAILED

Failed-ACH variant; treated as overdue.

None (data value read by overdue queries)

UNCOLLECTABLE

No valid Plaid account and no valid primary debit card; re-evaluated on every Daily Retry run.

Daily Retry

DEFAULTED

Terminal: more than 90 days past due, bannable ACH return, chargeback, or user ban.

Daily Retry; ACH Handler; ban endpoint

HOLD

Collection paused; scheduled batch jobs skip the float.

External tooling (checked in pkg/collections/jobs/rules.go)

Source: pkg/rds/status.go (which also defines CHARGED_BACK — a payment-response value, not a float status: on a chargeback event the ACH Handler sets the float to DEFAULTED and records CHARGED_BACK as the collection outcome; see ACH Callbacks)

The overdue set — statuses targeted by Daily Retry and the webhook collection paths — is RETRY, FAILED, ACHFAILED, UNCOLLECTABLE. Source: pkg/collections/status.go.

Creation & Disbursement

POST /{user_id}/floats runs all checks synchronously; the float record is written to RDS only after the disbursement succeeds.

  1. Validate the user (User Service): state not restricted (floats.restricted_states), status ACTIVE, email verified where enforced — otherwise 400.

  2. Validate subscription (Subscriptions Service): PAST_DUE or STALE returns 400.

  3. Acquire the user’s distributed lock (locks table).

  4. Fetch the linked bank account (Transactions Service).

  5. Check for an active requirements bypass (Requirements Bypass).

  6. Run fraud checks — see Fraud Detection; a connectivity error returns 500.

  7. Fetch the float profile and fee (Underwriting Service); float not enabled for the user returns 400.

  8. Fetch next payday and payback date (Insight Service); a custom payback date is validated against the allowed window.

  9. Underwriting eligibility check — skipped when a bypass is active; not approved returns 400.

  10. Submit the disbursement (Payments Service) as a PINLESS, RTP, or ACH credit; an RTP rejection (reported by Payments as a P04 error) retries as PINLESS only when floats.can_fallback_pinless is enabled for the app build.

  11. Write the float to RDS: status SCHEDULING, the credit confirmation ID, the computed due date (ach_debit_date), and next_payday_date (first payday after the due date).

  12. Send the confirmation email (Segment + Iterable), publish user_float_created (Events), return 201.

If the disbursement succeeds but the RDS write fails, funds have moved with no float record. The confirmation email and event publish happen only after the RDS write.

Manual Payback

POST /{user_id}/floats/{float_id}/payback with payback_option: "ACH" | "PINLESS" pays back a single float early.

  1. Fetch the float; it must be at least 24 hours old and not COMPLETED or ACHSENT — otherwise 404.

  2. Acquire the user’s billing lock.

  3. Calculate the fee (PINLESS/RTP floats).

  4. Submit the payment (Payments Service) per payback_option.

Payback submissions set bypass_retry_limit, so member-initiated attempts are never blocked by the Payments Service pinless retry limit.
Result Float status Response

Error or FAILED

RETRY; collection log process=MANUAL

500

ACH submitted

ACHSENT; collection log process=MANUAL; finalized asynchronously by the ACH Handler (ach_processing.adoc#ach-callbacks)

202

Pinless collected

COMPLETED; collection log process=MANUAL; digital receipt (Segment, Iterable, AppsFlyer); underwriting recalculation

202

Reactivation Payback

POST /{user_id}/floats/payback/reactivate pays off all past-due floats before a cancelled account is reactivated. Pinless debit only, regardless of loan type; no request body.

  1. Fetch the user’s active floats (RETRY, DEFAULTED, SCHEDULING, ACHSENT, UNCOLLECTABLE) and filter to past-due (ach_debit_date ⇐ now).

  2. Total responsibility = sum of amount + fee across the past-due floats; the expected amount is validated before each submission.

  3. Per float, sequentially: submit the reactivation payback (Payments Service; retry-limit bypass applies). ErrFloatNotPayable logs a warning, subtracts the float from the total, and continues; any other error returns 500. A result other than COMPLETED stops the run with 402 Payment Required and {AmountCollected, AmountRemaining, MaskedCardNumber}.

  4. All collected: 200 with {AmountCollected, AmountRemaining, AmountTotal, MaskedCardNumber}.

A 402 means partial collection — at least one float was paid but another could not be; the caller uses the amounts to decide whether reactivation proceeds.

Requirements Bypass

A per-user record that lets float creation skip the underwriting eligibility check. Granted by internal operations tooling via the bypass endpoints (see API spec); stored in the requirements-bypass DynamoDB table keyed by user_id (schema).

While an unexpired bypass exists:

  • The underwriting eligibility check is skipped.

  • Fraud checks still run, with the Bypassed flag passed to the Payments Service; fraud results are not saved back for bypassed users.

The record expires on its expiration_date but is not deleted — it remains in DynamoDB as inactive until an explicit DELETE.

User Ban

POST /{user_id}/floats/ban defaults any float that could still be collected. For each active float (RETRY, DEFAULTED, SCHEDULING, ACHSENT, UNCOLLECTABLE):

  • RETRY or SCHEDULING → status DEFAULTED (debit ID user_banned); collection log process=SUPPORT, outcome=DEFAULTED.

  • ACHSENT, DEFAULTED, UNCOLLECTABLE → unchanged. An in-flight ACH debit is not cancelled; the ACH Handler finalizes it on settlement.

No active floats returns 200 as a no-op.

Default & Uncollectable

DEFAULTED is set on four conditions. The first three are reached by a collection run; the fourth is a support action outside the collections engine:

  • Age — the float is more than 90 days past due (Daily Retry).

  • Bannable ACH return — the debit is returned with a code NACHA prohibits re-initiating; the float defaults and the user is banned (ach_processing.adoc#bannable-return-codes).

  • Chargeback — a chargeback event on the prod-payments stream; the float defaults and the user is banned.

  • User ban — the ban endpoint defaults the user’s RETRY and SCHEDULING floats directly, writing a SUPPORT collection log; no collection run is involved. See User Ban.

UNCOLLECTABLE is set by Daily Retry when the user has neither a valid Plaid account nor a valid primary debit card. It is not terminal: each Daily Retry run re-evaluates these floats, and collection resumes if a valid payment method appears.