user-stories
US-03 — Sending Payments
US-03 — Sending Payments
User initiates a crypto send to an external address or a saved payee. The system runs validation, AML, and any required step-up checks, then the transaction settles on-chain.
Persona
Any signed-in user — typically individual, but companies use the same flow for ad-hoc payments outside the ../03-Domains#Payouts surface.
Goal
"Send 250 USDC on ETH to my contractor's wallet, with a small memo, and know it's actually moved before I close the page."
Preconditions
- User is signed in, not blocked, KYC
approved. - User has sufficient balance for
amount + network fee + platform feein the chosen asset under the MVP mock model. Live token sends may also require a separate gas asset, e.g. ETH forUSDT_ETH; see ../09-Open Questions#Gas asset funding for token transfers. - The destination address parses for the chosen network (regex check, server + client).
- User can complete any step-up challenge (TOTP / passkey) required by the send policy.
Happy path
- User goes to
/send, picks asset →/send/[asset]. - User enters destination address (or picks a saved payee →
/send/[asset]/[payeeId]) and amount. - Server-side validation:
- Address parses for the network.
- AML lookup via
lib/aml/index.ts(cached per(address, network, currency)for 24 h, see ../05-External Providers#Mocked Elliptic). - If score ≥ block threshold → reject with reason; if ≥ warn threshold → confirm dialog.
- User reviews
/send/confirm(app/my/send/confirm/page.tsx) showing:- Amount, network fee estimate, platform fee (resolved via ../03-Domains#Fees).
- Total debit in fiat equivalent.
- Open for live cutover: network-fee commission policy for individual sends, separate from Mass Payouts. See ../09-Open Questions#Individual outbound transfer network-fee commission.
- If step-up verification is required → user completes the TOTP / passkey challenge.
- On confirm:
- Advisory lock per
(account, asset). - Insert
transactionsrow withtype='send',status='pending', idempotency key on retries. - Call
Fireblocks.createTransaction(...)— syntheticmock_<24hex>in mock mode. - Emit
send.initiatedemail + webhook.
- Advisory lock per
- Indexer flips
status='completed'when the chain confirms (orfailedon a 1% mock failure). /walletshows the outgoing transaction; balance reduced.
Alternate flows
- Internal send to another platform user: same flow, but the destination is matched against
walletsfirst; if matched, settlement is bookkeeping-only (no chain tx) and is instant. Fee schedule may differ. - Saved payee:
payeestable stores(label, address, network, asset). User picks from a list; address is read-only. - Memo / reference: optional, stored on the transaction row; passed to the chain when the asset supports it.
- Pay-to settlement: same primitive, triggered from the public
/pay/to/[slug]page on behalf of a payer. See ../03-Domains#Pay-to.
Edge cases & failure modes
- Insufficient balance: rejected at confirm with a precise shortfall amount; no transaction row written.
- Address fails network regex: rejected before AML lookup (saves API spend).
- AML provider down: send fails with
provider_unavailable; we do not fall through, by design (see ../08-Risks and Drawbacks#Domains). - AML score ≥ block threshold: send rejected,
audit_logrecords the score; user sees a generic "transfer rejected" message. - AML score in warn band: confirm dialog requires explicit "I accept the risk" tap; warning persists in the row metadata.
- Auth challenge rejected: the app rejects the attempt and records it against the DB-backed lockout window; deployed rate limits are also enforced by AWS WAF.
- Fireblocks failure (mock 1%): transaction row flips to
failed, balance returned, user sees error toast. - Network congestion: status stays
pendinguntil indexer confirms; UI shows "broadcasted, awaiting confirmation". - User blocked mid-flow: any in-progress send is rejected at confirm time (block flag is checked again).
Acceptance criteria
- Address validity is checked before any AML call (server + client).
- AML cache is keyed on
(address, network, currency)and reused across users. - Sends above the step-up threshold cannot proceed without additional verification.
- Idempotency key prevents double-debit on form resubmission.
-
audit_logrow written on initiate + status transitions. -
/walletreflects the send within one indexer cycle.
Out of scope
- Multi-recipient sends from this UI (use ../03-Domains#Payouts for that)
- Gas-token top-up flows (open: whether user holds the network gas asset, or Daxchain funds it through Gas Station / auto-fuel)
Drawbacks
- AML cache is shared across users — a hot address still costs one Elliptic call for the whole platform per 24 h, but cache is per-key, not per-merchant. ../08-Risks and Drawbacks#Payouts (same caveat applies to single sends).
- The send fee is a flat EUR amount (
platform_fees.send_eur_minor), not a percentage, so it does not scale with transfer size. ../08-Risks and Drawbacks#Fees
Open questions
- ../09-Open Questions#AML provider
- ../09-Open Questions#Sanctions
- ../09-Open Questions#Secrets at rest
- ../09-Open Questions#Individual outbound transfer network-fee commission
- ../09-Open Questions#Gas asset funding for token transfers
Implementing surfaces
- UI:
app/my/send/page.tsx,app/my/send/[asset]/page.tsx,app/my/send/[asset]/[payeeId]/page.tsx,app/my/send/confirm/page.tsx,app/my/send-receive/confirm/page.tsx,app/my/send/send-form.tsx - AML:
lib/aml/index.ts,lib/providers/mock-elliptic.ts - Fees:
lib/fees/rates.ts(resolveSendFeeEurMinor),lib/fees/network.ts - Schema:
transactions(typesend),payees,payee_wallets,aml_records - Domain doc: ../03-Domains#Send