---
url: /docs/stablecoin/payouts.md
description: >-
  Execute a payout on EVM, Stellar, or Solana, including per-network
  authorization, minting USDB for testing, and the status lifecycle.
---

A payout executes the transfer locked in by a [payout quote](/stablecoin/payout-quotes): stablecoins move out of a funding source and the equivalent fiat lands in a customer's [bank account](/stablecoin/bank-accounts). This page covers the self-custodied path, pulling tokens from an external [blockchain wallet](/stablecoin/blockchain-wallets) you or your customer controls, including the per-network authorization mechanics and minting the test token.

If the funding source is a [managed wallet](/stablecoin/wallets) instead, BlindPay already custodies the balance, so there is no authorization step: skip straight to executing the payout with the wallet's address as `sender_wallet_address`.

## How it works

Every network authorizes a payout differently before it can execute:

| Network | Test network | Authorization mechanism |
| --- | --- | --- |
| EVM (Ethereum, Base, Polygon, Arbitrum) | Base Sepolia | ERC-20 `approve` on the token contract, scoped to the quoted amount |
| Stellar | Stellar Testnet | Call the authorize endpoint for an unsigned XDR transaction, sign it, then create the payout |
| Solana | Solana Devnet | Prepare a token delegation transaction, sign and submit it, then create the payout |

The pattern is always the same: create a [payout quote](/stablecoin/payout-quotes), authorize the tokens for the chosen network, then create the payout before the quote expires (5 minutes by default).

## Mint USDB

USDB is BlindPay's test stablecoin. It only exists on development instances, across every development network, and you can mint as much as you need to simulate payouts.

### Mint on EVM chains

USDB on EVM chains is minted from the dashboard, not the API. The wallet needs testnet ether to pay gas. Get some from the [Base Sepolia faucet](https://www.alchemy.com/faucets/base-sepolia).

Open `https://app.blindpay.com/instances/{instance_id}/utilities/mint` in your browser, replace `{instance_id}` with your instance ID, and mint USDB on `Base Sepolia`.

### Mint on Stellar

USDB can also be minted on `Stellar Testnet`.

### Create an asset trustline (one time only)

Before your wallet can receive USDB, it needs a trustline to the USDB asset. Do this once per wallet.

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/create-asset-trustline \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "address": "YOUR_ADDRESS"
  }'
```

This returns an unsigned XDR transaction:

```json
{
  "success": true,
  "xdr": "AAAAA..."
}
```

### Sign and submit the trustline transaction

You have two options, pick one:

* **Sign and submit it yourself**: sign the XDR with your Stellar wallet (or [Stellar Lab](https://lab.stellar.org/transaction/sign)) and submit it to the network directly.
* **Let BlindPay submit it**: sign the XDR with your Stellar wallet, then pass the resulting `signedXdr` to the mint endpoint in the next step and BlindPay submits the trustline transaction for you.

### Mint USDB tokens

Once the trustline exists, mint USDB to your wallet address. Pass `signedXdr` only if you chose the second option above.

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/mint-usdb-stellar \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "address": "YOUR_WALLET_ADDRESS",
    "amount": "1000000000000000000",
    "signedXdr": "YOUR_SIGNED_XDR"
  }'
```

### Mint on Solana

USDB can also be minted on `Solana Devnet`. Pass your wallet `address` and the `amount`:

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/mint-usdb-solana \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "address": "YOUR_WALLET_ADDRESS",
    "amount": "100"
  }'
```

This returns the on-chain signature:

```json
{
  "success": true,
  "signature": "5xY..."
}
```

## Prerequisites

## Create a payout on EVM chains

All stablecoins on EVM chains are ERC-20 tokens, so authorization means calling `approve` on the token contract to let BlindPay pull the quoted amount from the sender's wallet.

### Approve the tokens

Before approving, you need:

1. [An RPC provider URL for Base Sepolia](https://chainlist.org/?search=base\&testnets=true)
2. [A wallet funded with testnet ether](https://www.alchemy.com/faucets/base-sepolia) to pay gas
3. [The private key, or any way to instantiate the wallet with ethers.js](https://docs.ethers.org/v6/api/wallet/#BaseWallet_new)
4. The ERC-20 token contract address, ABI, and BlindPay's contract address, all returned in the [payout quote](/stablecoin/payout-quotes) response

This example uses an [Express](https://expressjs.com/en/starter/hello-world.html) server and [ethers.js](https://docs.ethers.org/v6/) to create a quote and approve it. Make sure [Node.js](https://nodejs.org/en/download/) is installed.

Create a folder, initialize it, and install the dependencies:

```bash
npm init -y
npm install express ethers
```

Create `index.js` and replace the placeholder values with your own:

```js [index.js]
import express from 'express'
import { ethers } from 'ethers'

const app = express()

app.get('/', async (req, res) => {
  const rpcProviderUrl = '<Replace this>' // Get one at https://chainlist.org/?search=base&testnets=true
  const walletPrivateKey = '<Replace this>' // This wallet needs testnet ether and USDB to run the transactions below
  const instanceId = '<Replace this>'
  const blindpayApiKey = 'YOUR_API_KEY'
  const bankAccountId = 'ba_000000000000'

  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${blindpayApiKey}`,
  }

  // 1. Create a payout quote
  const fiftyDollars = 5000
  const quoteBody = {
    bank_account_id: bankAccountId,
    currency_type: 'sender',
    cover_fees: false,
    request_amount: fiftyDollars,
    network: 'base_sepolia', // also "sepolia", "arbitrum_sepolia", "polygon_amoy"
    token: 'USDB', // development instances only ever use USDB
  }
  const createQuote = await fetch(
    `https://api.blindpay.com/v1/instances/${instanceId}/quotes`,
    { headers, method: 'POST', body: JSON.stringify(quoteBody) }
  )
  const quoteResponse = await createQuote.json()

  // 2. Approve the tokens
  const provider = new ethers.JsonRpcProvider(
    rpcProviderUrl,
    quoteResponse.contract.network
  )
  const yourWallet = new ethers.Wallet(walletPrivateKey, provider)
  const contract = new ethers.Contract(
    quoteResponse.contract.address,
    quoteResponse.contract.abi,
    provider
  )
  const contractSigner = contract.connect(yourWallet)

  const result = await contractSigner.approve(
    quoteResponse.contract.blindpayContractAddress,
    quoteResponse.contract.amount
  )

  res.send({
    hash: result?.hash,
    quoteId: quoteResponse.id,
  })
})

app.listen(3000)
console.log('Express started on port 3000')
```

Run it:

```bash
node index.js
```

Visit `http://localhost:3000`. After a few seconds you should see a response like:

```json
{
  "hash": "0x1ab66830a4804d80251f01b9d31c054a42068f5783c80d165507cddce0ac78ca",
  "quoteId": "qu_lxrCXUOOyrem"
}
```

### Execute the payout

Once the approval transaction confirms, create the payout with the same `quote_id`.

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/payouts/evm \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "quote_id": "qu_000000000000",
  "sender_wallet_address": "YOUR_WALLET_ADDRESS"
}'
```

```js [index.js]
const response = await fetch(
  'https://api.blindpay.com/v1/instances/in_000000000000/payouts/evm',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      quote_id: 'qu_000000000000',
      sender_wallet_address: 'YOUR_WALLET_ADDRESS',
    }),
  }
)

const payout = await response.json()
```

`quote_id` can only back one payout; a second call with the same quote fails, create a new quote instead.

## Create a payout on Stellar

Stellar has no allowance mechanism, so the client constructs and signs a real payment transaction to BlindPay's treasury address.

### Authorize the payout

Call the authorize endpoint with the quote and the sender's wallet address. This does not consume the quote or create any record, it only returns an unsigned transaction hash for the client to sign.

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/payouts/stellar/authorize \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "quote_id": "qu_000000000000",
  "sender_wallet_address": "YOUR_WALLET_ADDRESS"
}'
```

The response is the unsigned transaction, ready to sign:

```json
{
  "transaction_hash": "AAA...AAA"
}
```

### Sign and submit the transaction

Install the Stellar SDK:

```bash
npm install @stellar/stellar-sdk
```

Sign the returned transaction with the sender's Stellar key and submit it to the network:

```js [index.js]
import {
  Horizon,
  Keypair,
  Networks,
  TransactionBuilder,
} from '@stellar/stellar-sdk'

const server = new Horizon.Server('https://horizon-testnet.stellar.org')
const sourceKeypair = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY)

// Rebuild the transaction returned by the authorize endpoint
const transaction = TransactionBuilder.fromXDR(
  transactionHash,
  Networks.TESTNET
)

// Sign it with the sender's key
transaction.sign(sourceKeypair)

// Submit it to Stellar
const result = await server.submitTransaction(transaction)

console.log(result.hash)
// Save result.hash, you need it to create the payout on BlindPay
```

### Create the payout

Create the payout with the quote, the sender's wallet address, and the signed transaction from the previous step. BlindPay independently re-validates the signed transaction against the quote (destination and amount) before dispatching the payout.

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/payouts/stellar \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "quote_id": "qu_000000000000",
  "signed_transaction": "YOUR_SIGNED_TRANSACTION",
  "sender_wallet_address": "YOUR_WALLET_ADDRESS"
}'
```

## Create a payout on Solana

Solana payouts need a token delegation before the payout executes: the sender delegates the quoted amount to BlindPay, then BlindPay pulls it during payout processing.

### Prepare the delegation transaction

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/prepare-delegate-solana \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "owner_address": "YOUR_SOLANA_WALLET_ADDRESS",
    "token_address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "50000000"
  }'
```

This returns a serialized transaction to sign:

```json
{
  "success": true,
  "transaction": "AQAAA..."
}
```

### Sign and submit the delegation

Install the Solana dependencies:

```bash
npm install @solana/web3.js bs58
```

```js [index.js]
import { Connection, VersionedTransaction } from '@solana/web3.js'
import { Buffer } from 'node:buffer'

const RPC_URL = 'https://api.devnet.solana.com' // Use a mainnet RPC in production
const connection = new Connection(RPC_URL, 'confirmed')

// Serialized transaction from the previous step
const serializedTransaction = 'AQAAA...'

async function signAndSubmitDelegation() {
  const transactionBuffer = Buffer.from(serializedTransaction, 'base64')
  const transaction = VersionedTransaction.deserialize(transactionBuffer)

  // Sign with the sender's wallet (a browser wallet like Phantom, or a local Keypair server-side)
  const signedTransaction = await window.solana.signTransaction(transaction)

  const signature = await connection.sendTransaction(signedTransaction)
  await connection.confirmTransaction(signature, 'confirmed')

  console.log('Delegation signature:', signature)
  return signature
}

signAndSubmitDelegation()
```

Run it:

```bash
node solana-delegate.js
```

### Create the payout

Once the delegation transaction confirms, create the payout the same way as EVM, at `/payouts/evm`.

```bash [cURL]
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/payouts/evm \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "quote_id": "qu_000000000000",
  "sender_wallet_address": "YOUR_SOLANA_WALLET_ADDRESS"
}'
```

## cover\_fees

`cover_fees` on the payout quote decides who absorbs the fee, and it's locked in before you authorize the tokens:

| `cover_fees` | Who pays | Effect |
| --- | --- | --- |
| `false` | Customer | Fees are deducted from the fiat the bank account receives (the common case) |
| `true` | Sender | Fees are added on top, sent as extra stablecoin, so the recipient gets the full quoted amount |

See [payout quotes](/stablecoin/payout-quotes) for the full request and response fields.

## Status lifecycle

| Status | Meaning |
| --- | --- |
| `processing` | Default on creation. Stablecoins are being pulled from the funding source and the fiat transfer is in flight. |
| `on_hold` | Held for review. SWIFT payouts always start here; ACH, wire, and RTP payouts can also be routed here for compliance review after the crypto is collected. |
| `completed` | The fiat landed in the recipient's bank account. Terminal. |
| `failed` | The payout did not complete (for example, a review timeout or a rejected compliance check). Terminal. |
| `refunded` | The stablecoins were returned to the funding source instead of being converted to fiat. Terminal. |

## Testing

Development instances complete payouts automatically. Force a `failed` or `refunded` outcome by setting the payout quote's `request_amount` to one of these sentinel values before authorizing and executing the payout:

| `request_amount` | Result |
| --- | --- |
| `66600` ($666.00) | `failed` |
| `77700` ($777.00) | `refunded` (EVM payouts also fire a real on-chain refund transaction) |
| any other amount | `completed` |

## Webhooks

| Event | Fires when |
| --- | --- |
| `payout.new` | A payout is created. |
| `payout.update` | A payout changes status (for example, `processing` to `on_hold`). |
| `payout.complete` | A payout reaches `completed`, `failed`, or `refunded`. |

## Related

* [Payout quotes](/stablecoin/payout-quotes): lock the exchange rate, fees, and the on-chain approval payload
* [Bank accounts](/stablecoin/bank-accounts): add and manage recipient bank accounts
* [Blockchain wallets](/stablecoin/blockchain-wallets): register the external wallet a payout authorizes from
* [Off-ramp](/stablecoin/send): the funding-source overview and per-network summary
* [Supported chains](/kb/supported-chains): the full chain and token matrix
