---
title: "How to add stablecoin payments to your wallet integration"
seoTitle: "Add stablecoin payments to a wallet integration in 9 steps"
description: "Connect the wallets you already run to bank deposits and local payouts: register addresses, open virtual accounts, quote, authorize, and track webhooks."
date: "2026-09-21"
updated: "2026-09-21"
category: "payments"
author: "BlindPay Team"
howto:
  name: "How to add stablecoin payments to an app that already has wallets"
  steps:
    - name: "Decide your flows"
      text: "Choose pay-in, payout, or both. Pay-ins only need the wallet to receive. Payouts need the wallet to authorize each quoted amount."
    - name: "Onboard the customer"
      text: "Run KYC for individuals or KYB for businesses. Quotes can only be created once the customer is approved."
    - name: "Link the customer's wallet"
      text: "Register the wallet address with a signed message on EVM, or by direct address for smart-contract wallets, Stellar, Solana, and Tron."
    - name: "Create a virtual account"
      text: "Issue US bank details linked to the wallet, so each bank deposit converts to USDC or USDT and settles to it as a payin."
    - name: "Quote, show, then execute"
      text: "Request a live quote, show the rate, fee, and received amount to the user, and execute before the quote expires."
    - name: "Authorize and send the payout"
      text: "Have the wallet authorize the quoted amount with the chain's method, then create the payout to a local rail such as Pix or SPEI."
    - name: "Listen to webhooks"
      text: "Verify each signed webhook, deduplicate events, and map payin and payout statuses to your order states."
    - name: "Handle failures"
      text: "Credit the wallet back on refunded payouts, escalate failed ones to support, and re-quote when a quote expires."
    - name: "Test in a sandbox and go live"
      text: "Run every path on testnets with a test stablecoin, including forced failures and refunds, then switch keys and networks for production."
faq:
  - q: "How do I add stablecoin payments to an app that already has wallets?"
    a: "Register each customer's wallet address with a stablecoin payments API, create virtual accounts so bank deposits convert and settle to those wallets, and pay out by requesting a quote that the wallet authorizes. The wallet provider keeps holding keys and signing. The payments layer handles bank rails, conversion, compliance, and payout status webhooks."
  - q: "Do I need to pre-fund a balance before sending stablecoin payouts?"
    a: "Not with a non-custodial payout flow. The stablecoins stay in the customer's own wallet, and the provider pulls only the quoted amount when the payout executes. The wallet does need to hold that amount at execution time, plus the network's gas token for the authorization transaction on most EVM chains. Arc is an exception, since it uses USDC for gas."
  - q: "Can a smart-contract wallet fund stablecoin payouts?"
    a: "Yes. Smart-contract wallets, such as ERC-4337 accounts or multisigs, can't produce the standard signed message used to prove ownership, so they are registered by submitting the address directly. For an EVM payout, the smart account then sends the ERC-20 approve for the quoted amount like any other wallet, through its own transaction flow."
  - q: "Why do stablecoin payment APIs use quote then execute?"
    a: "A quote locks the exchange rate and fees for a short window, so the user can confirm the exact amount the recipient will get before any money moves. If the quote expires, you request a new one and nothing has moved. Without the quote step, the rate shown on screen and the rate applied at execution can drift apart."
  - q: "What happens if a stablecoin payout fails?"
    a: "It depends on the final status. A refunded payout returns the stablecoins to the wallet that funded it, so your ledger should credit that wallet again. A failed payout, for example after a rejected compliance check, does not refund automatically and needs a follow-up with support. Don't retry a failed payout with a new quote until that is resolved."
  - q: "How do I test stablecoin payments without real money?"
    a: "Use a development instance on testnets with a test stablecoin. On BlindPay, development instances use USDB on networks like Base Sepolia and Solana Devnet, payins complete automatically about 30 seconds after creation, and payout amounts of $666.00 and $777.00 force failed and refunded outcomes, so you can test every path before launch."
---

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](/resources/more/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.

| Flow | Money moves | The wallet's job | Wallet signs? |
| --- | --- | --- | --- |
| Pay-in (collect) | Bank deposit, converted to USDC or USDT, delivered to the wallet | Receive | No |
| Payout (send) | Stablecoins from the wallet, converted, sent to a bank account | Authorize the quoted amount | Yes, once per payout |
| Both | A contractor platform funding by ACH and paying out over Pix | Receive, then authorize | For 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](/resources/more/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](/resources/more/how-to-automate-kyc-kyb-stablecoin-payments) covers the verification flow in detail.

## Step 3: Link the customer's wallet

**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](/docs/blockchain-wallets) (`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 type | Registration method | Why |
| --- | --- | --- |
| EVM wallet that can sign messages (most embedded, browser, and MPC wallets) | Signed message, `is_account_abstraction: false` | BlindPay recovers the address from the signature, which proves control |
| Smart-contract wallet (ERC-4337 account, multisig) | Direct address, `is_account_abstraction: true` | Contract wallets validate signatures through [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) and can't produce the standard signed message |
| Stellar, Solana, or Tron wallet | Direct address, `is_account_abstraction: true` | The 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](/docs/api/reference) for the full list.

```bash
# 1. Get the fixed message to sign
curl https://api.blindpay.com/v1/instances/in_000000000000/customers/re_000000000000/blockchain-wallets/sign-message \
  --header 'Authorization: Bearer YOUR_API_KEY'

# 2. Sign it with the customer's wallet, then register the wallet
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/customers/re_000000000000/blockchain-wallets \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Main wallet",
    "network": "base",
    "is_account_abstraction": false,
    "signature_tx_hash": "0x..."
  }'
```

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
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/customers/re_000000000000/virtual-accounts \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "banking_partner": "YOUR_ELIGIBLE_PARTNER",
    "token": "USDC",
    "blockchain_wallet_id": "bw_000000000000"
  }'
```

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](/docs/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](/resources/more/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
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/quotes \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "bank_account_id": "ba_000000000000",
    "currency_type": "sender",
    "cover_fees": false,
    "request_amount": 10000,
    "network": "base",
    "token": "USDC"
  }'
```

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](/resources/more/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.

| Network | How the wallet authorizes | Then |
| --- | --- | --- |
| EVM: Ethereum, Polygon, Base, Arbitrum, Tempo, Arc | An ERC-20 `approve` for the quoted amount, using the token and contract details returned in the quote | Create the payout at `/payouts/evm` with `quote_id` and `sender_wallet_address` |
| Stellar | `/payouts/stellar/authorize` returns an unsigned transaction for the wallet to sign | Create the payout at `/payouts/stellar` with the signed transaction |
| Solana | `/prepare-delegate-solana` returns a token delegation transaction for the wallet to sign and submit | Create the payout once the delegation confirms |

Who signs depends on your [integration model](/resources/more/wallet-api-vs-embedded-wallet-sdk-vs-white-label):

- **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](/resources/more/what-is-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](/docs/learn/webhooks)).

| Status | Payin meaning | Payout meaning | Terminal | Your order state |
| --- | --- | --- | --- | --- |
| `processing` | Waiting for the deposit, or converting and sending stablecoins | Pulling stablecoins, fiat in flight | No | Pending |
| `on_hold` | Held for manual risk or compliance review | Held for review; all SWIFT payouts start here | No | In review |
| `completed` | Stablecoins delivered to the wallet | Fiat landed in the recipient's account | Yes | Paid |
| `failed` | The payin did not go through | The payout did not complete | Yes | Failed, needs follow-up |
| `refunded` | The deposit was returned to the sender | Stablecoins returned to the funding wallet | Yes | Returned |

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](/resources/more/stablecoin-api-webhooks-reconciliation) covers reconciliation, and [payout statuses explained](/resources/more/stablecoin-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](/resources/more/stablecoin-api-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](/prompts) has ready-to-paste prompts for payin and payout quickstarts. [Build with AI](/docs/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](/docs/blockchain-wallets) and [payouts](/docs/payouts) docs have every field for steps 3 and 6.
