---
title: Wallets
description: Programmable wallets — per-agent fund pools with mandate-gated debits, multi-rail funding (Pix, TED, USD wire, USDC, BRLA), and automatic reconciliation. Part of the AgentGate managed tier.
---

import { Callout } from "fumadocs-ui/components/callout";

# Wallets

A **wallet** is a per-agent fund pool. The agent can spend only from this pool — never from the account's main account — and every spend is mandate-gated. Multi-rail funding (Pix, TED, USD wire, USDC, BRLA) lands on the same ledger; reconciliation against provider receipts runs automatically.

Wallets are part of **AgentGate**, the commerce-governance capability set in the managed tier. Self-host customers get a single-tenant reference wallet; the multi-tenant ledger, recon engine, and accounting export ship in the managed tier.

## Why wallets

Without a wallet primitive, agent spend either goes through the account's main account (no segregation) or against an abstract policy budget (no real funds, no real settlement). Both fail the auditor test for regulated enterprise customers.

A wallet adds three things at once:

1. **Funds segregation** — the agent has a per-pool balance the operator funded explicitly. Cross-tenant transfers are blocked at the database layer.
2. **Mandate gating** — every debit references a signed mandate with cap + expiry. The HMAC verification + cap check + the wallet's own available-funds check are three independent gates; bypassing all three requires a coordinated bug in three packages.
3. **Reconciliation** — every debit pairs with a provider receipt. The recon engine flags unmatched debits and orphan receipts so the operator sees mismatches without reading raw logs.

## The non-overridable invariants

Four invariants enforced at the database layer (not app code):

| Invariant | How it's enforced |
|---|---|
| **Wallet debits are mandate-gated** | `wallet_ledger` CHECK constraint: `kind IN ('hold','debit')` requires `mandate_id IS NOT NULL` |
| **Cross-tenant operations are blocked** | Every wallet route filters by `(org_id, project_id)` from `authContext`; foreign keys cascade via `org_id` |
| **Negative balance is impossible** | `wallet_balances` CHECK: `balance_minor >= 0` AND `available_minor >= 0` AND `available_minor <= balance_minor` |
| **Reconciliation anomalies require human review** | Debits older than grace with no matching provider event land in `wallet_recon_anomalies`; resolved by the operator, not the engine |

App-layer bugs cannot bypass these. A retried hold that would overdraw trips the CHECK; the transaction rolls back; the runtime returns a typed `InsufficientFundsError`.

## Lifecycle

```
                    ┌──────────────┐
                    │  fund        │  webhook from provider →
                    │  (kind=fund) │  bridge inserts ledger row
                    └──────┬───────┘  → balance + available go up
                           │
                           ▼
                    ┌──────────────┐
                    │  hold        │  gateway reserves available
                    │  (kind=hold) │  → available drops, balance same
                    └──────┬───────┘
                           │
              ┌────────────┴────────────┐
              ▼                         ▼
    ┌──────────────┐          ┌──────────────┐
    │  debit       │          │  release     │  abort path:
    │  (commit)    │          │  (rollback)  │  available restored,
    └──────┬───────┘          └──────┬───────┘  no money moved
           │                         │
           ▼                         ▼
    balance + available        balance + available
    drop by amount + fee       unchanged
```

Every entry is **append-only**. Corrections post a new `kind=reverse` row; existing rows are never updated except for `reconciled_at` (set by the recon engine).

## Funding rails

The funding bridge is **provider-agnostic**: every webhook adapter normalizes payments to `commerce.payment.{succeeded,refunded,failed}` and the same code path posts to the wallet ledger. A new provider is one adapter entry — wallet code untouched.

| Rail | Provider | Currency | Auth |
|---|---|---|---|
| Pix | Asaas | BRL | API key (`access_token` header) |
| Card | Stripe | USD / BRL | webhook signing secret |
| Card / Pix | Mercado Pago | BRL | HMAC over `id;request-id;ts` |
| Card / Pix | Zoop | BRL | HTTP Basic |
| **Stablecoin onramp** | BRL↔USDC onramp partner | USDC | HMAC over body |
| **Stablecoin onramp** | BRLA Digital | BRLA | HMAC over body |

To bind a connection as a funding source, see [`POST /v1/wallets/:id/funding-sources`](/docs/api/wallets#funding-sources). The dashboard panel at `/dashboard/wallets/{id}` does this visually.

## Execute lifecycle

`POST /v1/wallets/:id/execute` runs the full gateway lifecycle:

```
policy_check → mandate_check → wallet_hold → route_select →
payment_execution → wallet_settle (debit | release) → budget_record
```

Each step produces an audit entry. The dashboard's Execute modal renders the full audit trail inline — every gate (pass / fail / skip) with the detail string. This is the primary differentiation surface against Stripe Issuing or Brex when walking a CFO through a denied transaction.

| Outcome | HTTP | Wallet impact |
|---|---|---|
| `completed` | 200 | hold + release + debit (+ optional fee) |
| `requires-approval` (insufficient funds) | 402 | no hold posted; top up to retry |
| `denied` (policy or mandate fail) | 403 | no hold posted |
| `failed` (route or execute) | 422 | hold + release (no debit) |

## Reconciliation

A two-pass periodic engine (every 60s):

1. **Match debits to events** — unreconciled `kind=debit` rows older than the grace window get matched against `events.provider_event_id`. Hit → stamp `reconciled_at`. Miss → flag as `debit_without_receipt`.
2. **Flag orphan receipts** — `commerce.payment.*` events with no matching wallet ledger row are flagged as `receipt_without_debit` against the connection's bound wallet.

When the webhook never arrived (provider lost the event, delivery URL misconfigured), the engine can fall back to **provider-side fetch** — querying the provider's API directly for the receipt. Behind a per-environment flag (`WALLET_RECON_PROVIDER_FETCH=on`).

The dashboard recon panel auto-collapses on clean wallets — when there's nothing to triage, there's no panel. When the engine flags, the panel surfaces with **Resolve** / **Dismiss** buttons inline.

## Crash-recovery sweeper

Every 5 minutes a sweeper scans for holds older than `mandate.expires_at` OR a 24h hard ceiling, with no matching release or debit. Each survivor gets an automatic release. This catches:

- gateway crashed between hold and the settle step
- provider execute hung past timeout, no settle event arrived
- operator-side bug stranded a hold

A hold that the provider DID charge gets released by the sweeper AND surfaces in the recon engine as a debit-without-receipt orphan. The operator manually reconciles.

## Stablecoin entry path

USDC and BRLA enter via webhook adapters from our on/offramp partners. Both rails land on the same ledger as fiat — an enterprise agent operating in `BRL + USDC` sees one `wallet_balances` table with multiple `currency` rows.

Two wallet surfaces handle stablecoins differently today:

- **Account wallets** (`POST /v1/wallets/:id/execute`) do not route stablecoin currencies — `currency=USDC` or `currency=BRLA` returns `400 currency_not_routable`. Stablecoin balances on account wallets are entry-side only (webhook credits).
- **Consumer wallets** route stablecoins in production. A consumer holds ONE wallet with per-currency slots (for example `BRL` + `USDC`) minted from a single mandate signature, with per-slot caps and no FX inside the wallet: the payee type routes the payment to the matching slot (a URL / `0x` address routes to USDC, a Pix key routes to BRL). Balance moves between slots via the transfer planner. See the [consumer wallet API](/docs/api/wallets#consumer-wallets-multi-slot-mandate-wallet) for the endpoints and current rail status.

## Self-host parity

Per the VISION five-point MIT commitment, self-host customers get the **OSS reference wallet**: single-tenant, file-backed double-entry ledger, no settlement automation. The managed tier adds:

- Multi-tenant ledger with per-tenant scrypt-derived encryption keys
- Reconciliation engine (event match + provider-side fetch)
- Crash-recovery sweeper
- Per-tenant accounting export (CSV / SAP / NetSuite / Conta Azul)

Both tiers enforce the same four invariants. The managed tier adds operational primitives, not safety primitives.

## Next

- [Wallets API reference](/docs/api/wallets) — REST endpoints
- [Build an agent with a wallet (cookbook)](/docs/cookbooks/agent-with-wallet) — Asaas sandbox end-to-end
- [Authentication](/docs/concepts/authentication) — service-key vs bearer
- [Projects](/docs/concepts/projects) — wallets are project-scoped
