Dollar accounts in your app
Give customers a balance they fund by bank transfer, hold as stablecoins, and spend from.
Your customers hold a balance and spend from it. Deposits arrive as fiat and settle into a wallet you show them.
The shape
Two things fund the balance: a bank deposit that converts, and a plain on-chain send from anyone at all. Your product has to handle both, because the second one needs no cooperation from you.
Build order
| Step | Call |
|---|---|
| 1. Onboard | POST /api/customers with endorsements: ["endo_usd"] |
| 2. Submit | POST /api/customers/{id}/verifications/submit |
| 3. Create the wallet | POST /api/customers/{id}/wallets |
| 4. Create the funding account | POST /api/customers/{id}/virtual_accounts with {"kind": "wallet", "asset": "usdc", "wallet_id": "wal_..."} |
| 5. Show the balance | GET /api/wallets/{id} |
| 6. Show activity | GET /api/wallets/{id}/history |
| 7. Spend | POST /api/wallets/{id}/operations, then authorize |
The balance is a chain read
GET /api/wallets/{id} returns balances read from the chain at request
time, not from a ledger we keep. Three consequences for your product:
- Direct deposits are immediately spendable. Someone pays your customer by pasting the address into any wallet app, and the funds count the moment they land.
- Your records cannot diverge from reality, because the chain is the record.
insufficient_balanceis judged against the chain when an operation is created, not against a number you cached.
Balances ride single-GET responses only. GET /api/wallets returns the
same objects without a balances key, so list pages stay cheap. Use
GET /api/wallets/total_balances for a portfolio roll-up.
GET /api/wallets/{id}/history is an activity log, not the source of truth
for balances. Render the balance from the wallet object, and use history
for the transaction list beneath it.
Deposit addresses are permanent
Each wallet is one address on one chain, and the address never changes. Show it as a QR code and let customers reuse it, which is what they expect from every other wallet they own.
If you need the same address on several EVM chains later, address_wallet_id
extends an existing wallet's derivation onto another EVM chain. Solana keys
are not EVM keys, so this is EVM only on both sides.
What to watch
wallet.deposit_receivedis your most important event. It fires for money that arrived without any API call of yours, which is the only notification you get that a customer was paid.- Sends need your signing service to be up. If it is down, sends stop. Deposits, balances, and history are unaffected. Decide what your UI shows in that window before it happens.
- Every send is metered, and destination-chain gas passes through at cost plus your plan's markup. Deposits are free. See Fees & Quotes.
The design decision to make early
A dollar-account product is the case where the 30-second authorization window bites hardest, because a human is usually pressing the button.
Put the human decision before you create the operation. Your UI confirms the send, and only then does your backend create, verify, sign, and authorize in one synchronous block. Creating the operation while a confirmation dialog is open will expire it. See An operation expired before signing.