Money Movement
What this covers / who it's for. The money journeys a client experiences: accounts and balances, deposits (including travel-rule holds), payouts under dual control, conversion orders, exports and statements, and the master balance. Behavior and guarantees here; execution mechanics in Providers & rails, specifics in the module READMEs.
One principle shapes everything on this page: the finance rail executes; Stables orchestrates and mirrors. Funds live at the regulated provider (Conduit in V1). Every account, balance, transaction, order, and payout you see in Stables is our local mirror of the provider's truth, kept in sync by webhooks plus a scheduled reconciliation backstop — so what you see can briefly lag reality, but never diverges from it for long.
Accounts, wallets, balances
A tenant's customer gets virtual accounts (fiat, with deposit instructions) and wallets (crypto, chain-bound), both mirrored from the rail (accounts module).
A balance is a mirror, not a ledger we compute. Balances (available / pending / frozen
per asset) are projected wholesale from each provider snapshot — overwritten in full, never
incremented locally. If Stables ever disagreed with the rail about a balance, the rail would win
by construction.
Wallet signers and signing quorum are managed through MFA-protected ceremonies: every signer or quorum change demands step-up MFA in Stables and then completes out of band at a provider-hosted URL, where the signer proves possession of their own credentials. Stables never optimistically updates the roster — it waits for the provider to confirm.
Deposits — and the travel-rule hold
Deposits arrive as a mirrored transaction stream; statuses flow from the provider
(funding module). A deposit from a registered source
address simply progresses to completed.
A deposit from an unregistered address parks in a travel-rule window:
- The affected users get a durable RFI notification (inbox + email; it is a compliance notice, so it cannot be opted out of) with the deadline.
- Submitting the sender information is asynchronous: the API accepts it (HTTP 202) and the deposit's status updates when the provider confirms — the user watches the transaction, not a spinner.
- If the window lapses, the deposit becomes a failed transaction carrying the failure code
SENDER_INFO_TIMEOUTplus when and why it was held — "held" describes that failed deposit, not a status of its own. Compliance then takes over automatically as a side effect: a compliance case opens and a travel-rule record is created (Compliance & trust). Failed and cancelled deposits always carry the provider's failure/cancellation code and user-facing reason.
Payouts — dual control, then co-sign
A payout never moves on one person's say-so. Three independent controls stand between "I want to pay" and moved funds (payments module):
- Make. The maker creates the payout with every money-material field mandatory. It sits at
pending_approval— the provider has not been called; nothing can move yet. - Check. A different user approves — self-approval is rejected
(
SELF_APPROVAL_FORBIDDEN), and the approval demands fresh step-up MFA plus a written rationale. The submission body is rebuilt only from the fields the approver reviewed; if the stored request carries anything that differs from the reviewed values, approval fails withPAYOUT_BODY_TAMPERED— and that rejected attempt still lands in the audit trail. A payout the rail rejects reverts topending_approvalrather than getting stuck. - Co-sign. The rail then requires an out-of-band signature at its own hosted
verification_url— outside Stables entirely, so compromising Stables alone cannot move funds.
Internal approval statuses: pending_approval → submitting → approved (or rejected /
cancelled). Provider execution ends at completed, failed, or cancelled. The full status
vocabulary is in the glossary.
Orders, registered addresses, whitelist recipients
- Orders are on/off-ramp conversions (fiat ↔ stablecoin), executed by the rail and mirrored
back; terminal statuses are
succeeded/failed/cancelled. - Registered addresses whitelist deposit sources — the flow that keeps future deposits from those addresses out of the travel-rule hold.
- Whitelist recipients are pre-vetted fiat payout destinations. Their bank details (account/IBAN/routing numbers) are encrypted at rest and never returned by the API.
Exports & statements — deliberately not instant
Transaction exports and statements follow the request-then-poll pattern (Async by design):
| Surface | How it behaves |
|---|---|
| On-demand export | POST accepts immediately (202) with a pollable request id; when it succeeds, the result is a short-lived, permission-gated download link — never a permanent URL. |
| Monthly statements | Generated automatically for every eligible tenant on the 1st of each month at 02:00, as a tracked background run operators can inspect; clients discover them via the statements endpoint, which mints fresh signed links on each read. |
The master balance
The treasury module answers "how much do I have, in total?" — it sums every asset's balance and converts each into one base currency with exact decimal math. Honest limits, stated by the API itself:
- V1 supports a USD base only; any other requested base is cleanly refused.
- The response carries
rates_as_of(how fresh the conversion rates are — kept within ~20 seconds) andrates_provisional: truetoday — V1's rates come from a placeholder configuration, not a live market feed, and the flag says so explicitly so a UI can render the total as an estimate. An asset that cannot be priced is reported unconverted rather than silently guessed.