How to add stablecoin payments to your wallet integration

Connect the wallets you already run to bank deposits and local payouts: register addresses, open virtual accounts, quote, authorize, and track webhooks.

To add stablecoin payments to an app that already has wallets, register each customer's wallet address with a payments API, route bank deposits into it through virtual accounts, and pay out from it with a quote that the wallet authorizes. The wallet keeps holding keys and signing. The payments layer handles bank rails, conversion, and compliance.

This tutorial assumes the wallets already exist, from an embedded SDK, a wallet API, or your users' own wallets. How to integrate a stablecoin API covers the full flow from a blank project. This guide covers the seams between the wallet you have and the payments you're adding: address registration, who signs what, and how failures land back in the wallet.

Step 1: Decide your flows: pay-in, payout, or both

Outcome: you know which direction money moves, and what the wallet has to do in each direction.

A pay-in collects bank money and delivers stablecoins to a wallet. A payout takes stablecoins from a wallet and delivers local currency to a bank account. Most products need both eventually, but the first release rarely does.

FlowMoney movesThe wallet's jobWallet signs?
Pay-in (collect)Bank deposit, converted to USDC or USDT, delivered to the walletReceiveNo
Payout (send)Stablecoins from the wallet, converted, sent to a bank accountAuthorize the quoted amountYes, once per payout
BothA contractor platform funding by ACH and paying out over PixReceive, then authorizeFor payouts only

Pay-ins are the easier first release, because the wallet never has to sign anything. If your wallets are held by users on phones, starting with pay-ins lets you ship before you've designed a single signing prompt. What is crypto wallet integration explains the layers if you need the wider view.

Step 2: Onboard the customer (KYC or KYB) before any money moves

Outcome: every customer is verified before a quote can be created for them.

KYC, know your customer, verifies an individual's identity. KYB, know your business, verifies a company, its registration, and the people who own and control it. Stablecoin payment providers run both because they move money between bank accounts and blockchains for someone else.

On BlindPay, each of your users becomes a customer with a kyc_status. New customers start at verifying. With Standard KYC, the status typically moves to approved or rejected automatically within about 60 seconds, unless the compliance team needs a manual review. Payin and payout quotes require approved or approved_rfi, so gate your payment screens on that status, not on your own signup state.

Store the customer ID next to your internal user ID and the wallet provider's wallet ID. You'll need all three in every support ticket. How to automate KYC and KYB covers the verification flow in detail.

Outcome: each customer wallet is registered with the payments layer, and you've confirmed it belongs to that customer.

The payments layer needs to know which address to deliver pay-ins to and which address will fund payouts. BlindPay calls this a blockchain wallet (bw_...): an external wallet the customer controls, whose keys BlindPay never holds. There are two ways to register one, and the right one depends on the wallet type.

Wallet typeRegistration methodWhy
EVM wallet that can sign messages (most embedded, browser, and MPC wallets)Signed message, is_account_abstraction: falseBlindPay recovers the address from the signature, which proves control
Smart-contract wallet (ERC-4337 account, multisig)Direct address, is_account_abstraction: trueContract wallets validate signatures through EIP-1271 and can't produce the standard signed message
Stellar, Solana, or Tron walletDirect address, is_account_abstraction: trueThe signed-message flow is EVM-only

For the signed-message flow, fetch the message, have the wallet sign it, and submit the signature. The request below is illustrative, with field names from the docs; check the API reference for the full list.

Bash

The direct-address flow checks the address format, not ownership. So never take the address from a text field. Read it server-side from your wallet provider's API, where you already know which user owns it. A stablecoin delivered to the wrong address can't be recovered by anyone. A blockchainWallet.new webhook fires when the wallet is created.

Step 4: Create a virtual account so bank deposits settle to the wallet

Outcome: the customer has US bank details, and every deposit lands in their wallet as USDC or USDT.

A virtual account is a bank account issued in the customer's name, with its own routing and account number. Each virtual account belongs to one customer and settles to one wallet, set by blockchain_wallet_id. A customer can hold more than one. Each deposit creates a payin, which you track like any other payment.

Bash

Plan for a review before the account works. A new virtual account starts in pending_review for compliance review, moves to verifying for the banking partner's approval, and only shows rail numbers once approved. US virtual accounts accept ACH, wire, and SWIFT deposits, and each costs $1.50 per month per account (virtual accounts). If you settle in USDT, the linked wallet has to be on a network that supports it.

For one-off collections instead of a standing account, create a payin quote with blockchain_wallet_id and share the payment instructions it returns, such as a Pix code or a SPEI CLABE. Stablecoin virtual accounts explained compares the two.

Step 5: Request a live quote, show the rate and fees, then execute

Outcome: the user sees the exact rate, fee, and received amount before anything moves.

A quote locks the exchange rate and fees for a payout. On BlindPay, a payout quote is created against a bank account and a network and token pair, and it expires 5 minutes after creation. Amounts are integers in minor units, so 10000 means $100.00.

Bash

Quote-then-execute prevents surprise pricing. The user confirms a firm number, the rate can't drift between the confirmation screen and execution, and an expired quote costs nothing: request a new one, since no funds moved. Show the fee breakdown from the response, not a number you computed yourself. Stablecoin API quotes explained covers expiry, fee direction, and minor units.

Step 6: Authorize the payout from the wallet and send it to a local rail

Outcome: the wallet authorizes exactly the quoted amount, and local currency goes out on the recipient's rail.

This is the step where your wallet integration and the payments layer meet. The authorization method depends on the chain.

NetworkHow the wallet authorizesThen
EVM: Ethereum, Polygon, Base, Arbitrum, Tempo, ArcAn ERC-20 approve for the quoted amount, using the token and contract details returned in the quoteCreate the payout at /payouts/evm with quote_id and sender_wallet_address
Stellar/payouts/stellar/authorize returns an unsigned transaction for the wallet to signCreate the payout at /payouts/stellar with the signed transaction
Solana/prepare-delegate-solana returns a token delegation transaction for the wallet to sign and submitCreate the payout once the delegation confirms

Who signs depends on your integration model:

  • Embedded SDK: the user sees a signing prompt in your app. Show the amount and the recipient on the same screen, in plain language.
  • Wallet API or MPC: your backend requests the signature. Add BlindPay's contract and treasury addresses to your policy engine's allowlist, and cap each approval at the quoted amount so a bug can't approve more.
  • The user's own wallet: request the signature through the wallet's standard provider interface.

Gas is the other detail. An EVM approve is an on-chain transaction, so the wallet needs the network's gas token, except on Arc, where USDC is the gas token. Payouts then go out over Pix, SPEI, ACH, RTP, SEPA, SWIFT (POBO/COBO), TED, ACH Colombia, and Transfers 3.0, depending on the recipient's bank account.

If your wallet can't sign contract approvals at all, because of policy limits or a custody setup that only sends plain transfers, use an off-ramp wallet instead: a deposit address that converts each deposit and pays the linked bank account.

Step 7: Listen to webhooks and update order state

Outcome: your order state follows the payment, without polling.

Payins and payouts move through the same five statuses, and each change fires a webhook: payin.new, payin.update, and payin.complete for deposits, and payout.new, payout.update, and payout.complete for payouts. Every call is signed, so verify the svix-id, svix-timestamp, and svix-signature headers before trusting a payload (webhooks).

StatusPayin meaningPayout meaningTerminalYour order state
processingWaiting for the deposit, or converting and sending stablecoinsPulling stablecoins, fiat in flightNoPending
on_holdHeld for manual risk or compliance reviewHeld for review; all SWIFT payouts start hereNoIn review
completedStablecoins delivered to the walletFiat landed in the recipient's accountYesPaid
failedThe payin did not go throughThe payout did not completeYesFailed, needs follow-up
refundedThe deposit was returned to the senderStablecoins returned to the funding walletYesReturned

Webhooks can arrive twice or out of order. Store each event ID, ignore duplicates, and never move an order backward from a terminal state. Send an Idempotency-Key header on every POST, so a retried request replays the original response instead of creating a second payout. Stablecoin API webhooks covers reconciliation, and payout statuses explained covers each status in depth.

Step 8: Handle failures

Outcome: every failure path ends in a known state with a known next action.

Payments that can't settle don't all come back the same way, so don't promise users that they do. Handle each case on its own:

  • Refunded payout: the stablecoins return to the wallet that funded the payout. Credit that wallet in your ledger and tell the user.
  • Failed payout: the payout doesn't refund automatically, for example after a rejected compliance check. Open a support case and hold the order. Don't retry with a new quote until it's resolved, or you risk paying twice.
  • Refunded payin: the bank deposit was returned to the sender. Nothing reached the wallet.
  • Expired quote: request a new quote. No funds moved.
  • Wrong address: an on-chain transfer to the wrong address can't be reversed. Prevention lives in step 3.

Step 9: Test in a sandbox and go live

Outcome: every path, including failures, has run end to end before a real dollar moves.

A BlindPay development instance runs on testnets with USDB, a test stablecoin with no mainnet deployment. Payins complete automatically about 30 seconds after creation. Payout quotes for $666.00 end failed and $777.00 end refunded, so you can exercise the unhappy paths on purpose. On stellar_testnet and solana_devnet you can mint USDB straight into a wallet. Tron has no testnet, so test Tron flows carefully in production with small amounts.

Before launch, check each item:

  1. Wallet registration uses signed messages wherever the wallet supports them.
  2. Direct-address registration reads addresses from your wallet provider's API, never from user input.
  3. Payment screens are gated on an approved customer status.
  4. The quote screen shows the fee breakdown and expiry from the API response.
  5. Signing policies allowlist the payout contract and cap approvals at the quoted amount.
  6. Webhook signatures are verified, and duplicate events are ignored.
  7. Every POST sends an Idempotency-Key.
  8. Forced failed and refunded payouts both land in the right order state.
  9. Production networks, tokens, and API keys replace the testnet ones, in a single config change.

Sandbox vs production lists what testing on testnets can't catch.

If you build with an AI coding assistant, point it at the same flow. BlindPay's MCP server (npx -y @blindpay/mcp) exposes the API as tools, the agent skills teach it the rails and gotchas, and the prompt library has ready-to-paste prompts for payin and payout quickstarts. Build with AI has the setup.

What to do next

Start with step 1 and ship pay-ins first: register wallets, open virtual accounts, and listen for payin.complete. Once deposits land reliably, add payouts one chain at a time, beginning with the chain most of your wallets already use.

The blockchain wallets and payouts docs have every field for steps 3 and 6.

FAQ