Deposit — Onramp flow

Flow
fiat → USDT (onramp)
Status
Shipped · 5 countries
Works
MX (SPEI) · BR (PIX) · AR (COELSA) · CO (ACH-PSE) · DO (ACH_DOM)
User pays fiat off-platform; we credit USDT on Alfred's ON_CHAIN_COMPLETED webhook. QA never moves real bank money — fire POST /psp/alfred/__dev/emit-onramp with scenario: "success" from the Dev playground to walk the entire chain.

Sequence — fresh-customer happy-path deposit

Step-by-step actor sequence. Color-coded lanes show who does what.

Scenario A — MX / BR fresh-customer happy-path deposit
QA
Widget
vakotrade BE
Alfred
  1. 1
    QA
    Fire __dev/seed-kyc-and-provision?country=MX → Alfred customer created + KYC text submitted.
  2. 2
    QA
    Fire __dev/approve-kyc → pushes KYC COMPLETED webhook to Alfred. Customer transitions to statusKyc: COMPLETED.
  3. 3
    Widget
    User opens widget → GET /eligibility → returns { eligible: true }. Deposit form renders.
  4. 4
    Widget
    User picks 500 MXN. POST /quote-preview shows ≈ 18 USDT.
  5. 5
    Widget
    User clicks Continue → POST /sessionPOST /initiate-deposit. Bank instructions + virtual address returned.
  6. 6
    QA
    Copy session_id from response. Fire __dev/emit-onramp with scenario: success.
  7. 7
    Alfred
    Webhook chain fires: FIAT_DEPOSIT_RECEIVED → TRADE_COMPLETED → ON_CHAIN_INITIATED → ON_CHAIN_COMPLETED.
  8. BE
    Result: DepositManualCreateJob dispatched. User balance += 18 USDT. Widget stepper shows all 4 checkmarks.
Scenario C — AR / CO / DO deposit (regression-fix verification)
QA
Widget
vakotrade BE
Alfred
  1. 1
    QA
    Setup with country=AR (or CO, or DO). For CO, supply a valid bank_id (e.g. 1007 Bancolombia).
  2. 2
    Widget
    GET /eligibility passes. Deposit form renders.
  3. 3
    Widget
    User clicks Continue → POST /initiate-deposit.
  4. 4
    Alfred
    Alfred returns 201 with a real transaction + bank-payment instructions (Bug #1 fix verified 2026-05-29).
  5. 5
    QA
    Fire __dev/emit-onramp with session_id to walk the webhook chain.
  6. Widget
    Expected: balance credited with to_amount USDT. Same outcome as MX / BR scenarios.

End-to-end deposit flow

The full happy path from widget mount to credited balance. Steps marked QA use dev endpoints; everything else is the production widget path.

Deposit (Onramp)
fiat → USDT
User pays fiat off-platform; we credit USDT on Alfred's ON_CHAIN_COMPLETED webhook.
1
Widget mount
FE calls GET /psp/alfred/eligibility. Returns { eligible: true } or throws USER_NOT_VERIFIED / ALFRED_KYC_PENDING / ALFRED_KYC_REJECTED / ALFRED_NOT_AVAILABLE_FOR_COUNTRY / ALFRED_CUSTOMER_NOT_PROVISIONED.
2
Pick amount + rail
GET /psp/alfred/supported-options returns the rail list; POST /psp/alfred/quote-preview returns live fiat→USDT preview while the user types.
3
Create session
POST /psp/alfred/session re-runs KYC + Alfred KYC + country + customer-provisioned checks; on pass returns a session_id (24h TTL).
4
Initiate deposit
POST /psp/alfred/initiate-deposit mints an Alfred quote, calls /onramp, returns the per-tx virtual deposit address + fiat payment instructions. For CO must include bank_id (4-digit PSE).
5
User pays bank → Alfred webhook chain
FIAT_DEPOSIT_RECEIVED → TRADE_COMPLETED → ON_CHAIN_INITIATED → ON_CHAIN_COMPLETED.
QA POST /psp/alfred/__dev/emit-onramp with scenario: "success" walks the entire chain without a real bank transfer.
6
Credit Payment
On ON_CHAIN_COMPLETED our handler validates the amount (Number.isFinite(amount) && amount > 0), runs a DB-uniqueness check on remote_txid, and enqueues DepositManualCreateJob. Widget polls GET /psp/alfred/session to read the new transactionStatus.

Per-country test cases

Deposit / Onramp — MX SPEI · BR PIX · AR COELSA · CO ACH-PSE · DO ACH_DOM.

TC-D1
MX user · full happy path · simulate ONRAMP success via dev endpoint
expect: Payment row created, balance += to_amount USDT
PASS
TC-D2
BR user · same flow as TC-D1 with PIX
expect: 201 onramp, ON_CHAIN_COMPLETED credits USDT
PASS
TC-D3
AR user · initiate-deposit + COELSA chain
expect: 201 onramp; balance += to_amount USDT after ON_CHAIN_COMPLETED
PASS
TC-D4
CO user without bank_id
expect: 422 / 110002 "bankId is required in metadata for COP currency"
EXPECTED
TC-D5
CO user with valid bank_id=1007 (Bancolombia)
expect: 201 onramp with PSE redirectUrl in fiatPaymentInstructions; widget renders inline iframe
PASS
TC-D6
DO user · ACH_DOM deposit
expect: 201 onramp; balance += to_amount USDT after ON_CHAIN_COMPLETED
PASS
TC-D7
Replay ONRAMP webhook with same referenceId
expect: log ALFRED_DEPOSIT_ALREADY_RECORDED, no double-credit (DB uniqueness on remote_txid)
PASS
TC-D8
Maintenance mode on, then call initiate-deposit
expect: throws ALFRED_MAINTENANCE_MODE
PASS
CO needs a PSE bank_id. CO requires metadata.bankId as a 4-digit PSE code on every POST /onramp. Without it Alfred returns 422 / 110002 "bankId is required in metadata for COP currency". The widget already enforces this on the deposit form.
Bug #1 closed 2026-05-29. AR / CO / DO onramp previously returned 400/111301 UNKNOWN_ERROR. Alfred shipped the fix; TC-D3 / TC-D5 / TC-D6 are now PASS. Keep them as regression coverage on every retest.

curl — full happy path (MX)

# 1. session
POST /psp/alfred/session
{ "data": { "type":"deposit", "amount":500, "source_currency_id":"MXN", "payment_route_id":"<ALFRED-MXN-USDT>" } }

# 2. initiate
POST /psp/alfred/initiate-deposit
{ "session_id":"<id>" }
→ 200 { depositAddress, transaction, quote }

# 3. QA dev-fast-forward
POST /psp/alfred/__dev/emit-onramp
{ "session_id":"<id>", "scenario":"success" }
→ webhook chain → DepositManualCreateJob → credited
Run it. Open the Dev playground → configure base URL / JWT / dev secret once in the console → seed + approve a customer → grab the session_id from POST /psp/alfred/session → fire the emit-onramp runner with scenario: success and watch the balance change.
Fire the onramp runner
emit-onramp · seed-kyc-and-provision · provision-customer · approve-kyc.