user-stories

US-03 — Sending Payments

docs/user-stories/03-Send-Payment.mdtype: user-storyupdated: 2026-05-06

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 fee in the chosen asset under the MVP mock model. Live token sends may also require a separate gas asset, e.g. ETH for USDT_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

  1. User goes to /send, picks asset → /send/[asset].
  2. User enters destination address (or picks a saved payee → /send/[asset]/[payeeId]) and amount.
  3. 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.
  4. User reviews /send/confirm (app/my/send/confirm/page.tsx) showing:
  5. If step-up verification is required → user completes the TOTP / passkey challenge.
  6. On confirm:
    • Advisory lock per (account, asset).
    • Insert transactions row with type='send', status='pending', idempotency key on retries.
    • Call Fireblocks.createTransaction(...) — synthetic mock_<24hex> in mock mode.
    • Emit send.initiated email + webhook.
  7. Indexer flips status='completed' when the chain confirms (or failed on a 1% mock failure).
  8. /wallet shows the outgoing transaction; balance reduced.

Alternate flows

  • Internal send to another platform user: same flow, but the destination is matched against wallets first; if matched, settlement is bookkeeping-only (no chain tx) and is instant. Fee schedule may differ.
  • Saved payee: payees table 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_log records 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 pending until 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_log row written on initiate + status transitions.
  • /wallet reflects 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

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 (type send), payees, payee_wallets, aml_records
  • Domain doc: ../03-Domains#Send