> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Cash out to a bank with Bridge

Let a user move USDC out of their Openfort embedded wallet and into their own bank account. [Bridge](https://apidocs.bridge.xyz) (Stripe) issues a **liquidation address**: a permanent blockchain address tied to a verified customer. Anything sent to it is converted and paid out to the bank account behind it, by ACH or wire for USD and SEPA for EUR. You create it once, and every cash-out after that is an ordinary ERC-20 transfer from the wallet.

This is the reverse of [virtual bank accounts](https://www.openfort.io/docs/recipes/virtual-bank-accounts): that recipe issues bank details so fiat can arrive, this one issues an address so it can leave.

With Openfort and Bridge together, your app can:

* Verify the user through Bridge's hosted KYC flow
* Link a US bank account (routing and account number) or a European IBAN
* Give the user a permanent cash-out address and send USDC to it with sponsored gas
* Track each payout from `funds_received` to `payment_processed`

:::warning
Bridge's sandbox cannot demonstrate a payout. It has no testnet, liquidation addresses and transfers come back as dummy data, and no payment webhooks fire. The recipe simulates the payout timeline in sandbox, marks every simulated record `simulated: true`, and reads real payouts from Bridge in production.
:::

:::note
This recipe is not yet in the recipes-hub default branch. Its source is in review in [recipes-hub#63](https://github.com/openfort-xyz/recipes-hub/pull/63), and it has not been run against a Bridge account yet. Bridge sandbox access is not self-serve: email support@bridge.xyz for a developer account.
:::

## How it works

| Step | Call | Who does it |
| --- | --- | --- |
| Verify identity | `POST /v0/kyc_links`, then the hosted flow | Bridge creates the customer on approval |
| Link a bank | `POST /v0/customers/{id}/external_accounts` | Routing and account number, or IBAN and BIC |
| Get a cash-out address | `POST /v0/customers/{id}/liquidation_addresses` | Returns a permanent address |
| Cash out | ERC-20 `transfer` | The embedded wallet, gas sponsored |
| Track | `GET /v0/customers/{id}/liquidation_addresses/{id}/drains` | `funds_received` → `payment_submitted` → `payment_processed` |

Openfort handles the user and the wallet: sign-in, the embedded wallet, the sponsored transfer, and the server-side check that a request comes from the user who owns the wallet. Bridge handles identity, the bank account and the conversion.

### Trust nothing from the client

Every route handler verifies the Openfort session and looks the Bridge customer up from the user, never from the request body. The cash-out address is created with the user's own wallet as its return address, where Bridge sends funds if a payout fails, so the server first confirms the session owns that address:

```ts
// src/lib/auth.ts
export async function authorizeAddress(req: Request, address: string) {
  const { session, user } = await authenticateRequest(req)

  const { data: accounts } = await getOpenfort().accounts.list({ user: user.id })
  const normalized = getAddress(address)
  if (!accounts.some((account) => getAddress(account.address) === normalized)) {
    throw new AuthError('Address not owned by authenticated user', 403)
  }

  return { session, user }
}
```

`authenticateRequest` calls `openfort.iam.getSession()`, which returns `null` for an invalid or expired token in `@openfort/openfort-node` 0.12, so check for it before reading the user.

### Create the cash-out address

Once the customer is verified and has a linked bank account, one call returns the address:

```ts
// src/app/api/cash-out-address/route.ts
const liquidation = await createLiquidationAddress({
  customerId: record.bridgeCustomerId,
  userId: user.id,
  externalAccountId: bank.id,
  chain: 'base',
  rail: body.currency === 'eur' ? 'sepa' : 'ach',
  destinationCurrency: body.currency,
  returnAddress: body.returnAddress,
})
```

Bridge requires an `Idempotency-Key` on every POST. The recipe derives it from the operation and the Openfort user ID instead of a random value, so a double-clicked button returns the original bank account or address instead of creating a second one.

### Cash out

Cashing out is a USDC transfer from the embedded wallet to the liquidation address. With a gas sponsorship configured on the Openfort provider, the user needs no ETH:

```tsx
// src/features/bridge/use-cash-out.ts
const hash = await writeContractAsync({
  abi: ERC20_ABI,
  address: usdc,
  functionName: 'transfer',
  args: [cashOutAddress.address, parseUnits(amount, USDC_DECIMALS)],
})
```

In production Bridge sees the deposit, converts it and pays the bank. The app reads progress from the address's drains.

## Configuration

```bash
NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_OPENFORT_SHIELD_PUBLISHABLE_KEY=
OPENFORT_SECRET_KEY=sk_test_...                # Server-side: verifies the session before any Bridge call
NEXT_PUBLIC_OPENFORT_FEE_SPONSORSHIP_ID=       # pol_... on the chain below
NEXT_PUBLIC_OPENFORT_DEFAULT_CHAIN_ID=84532    # 84532 Base Sepolia, 8453 Base

BRIDGE_API_KEY=sk-test-...                     # Server-side only
BRIDGE_ENVIRONMENT=sandbox                     # sandbox or production
```

All Openfort keys and the gas sponsorship must come from the same project, and the sponsorship must cover the chain you run.

## Things to know

* **Bridge has no testnet chains.** `chain` is always a mainnet name such as `base`, so a sandbox address says `base` while the wallet runs on Base Sepolia.
* **The return address field** is `return_instructions: { address }` in Bridge's API reference; its offramp guide still calls it `return_address`. The recipe follows the reference.
* **KYC order.** The customer doesn't exist until Bridge approves the KYC link. Accept the terms-of-service link first; Bridge won't approve without it.
* **Embedded wallet only.** The server proves ownership by listing the user's Openfort accounts, so an externally connected wallet would be refused. The recipe registers only `embeddedWalletConnector()`.

## Next steps

* [Virtual bank accounts](https://www.openfort.io/docs/recipes/virtual-bank-accounts): the fiat-in counterpart
* [Gas sponsorship](https://www.openfort.io/docs/configuration/gas-sponsorship): sponsor the cash-out transfer
* [Bridge offramp guide](https://apidocs.bridge.xyz/get-started/guides/move-money/offramp_liquidation)
