---
title: Agent with a Wallet
description: End-to-end walkthrough of Programmable Wallets — create a wallet, bind an Asaas funding source, fund via sandbox Pix, execute a mandate-gated payment, watch the audit trail render. ~15 minutes.
---

<ActionBar githubPath="content/docs/cookbooks/agent-with-wallet.mdx" markdownPath="/api/docs-md/cookbooks/agent-with-wallet" />

<MetaStrip items={[
  { label: "TIME", value: "~15 min" },
  { label: "PROVIDER", value: (<><ServerChip name="Asaas" accent /><span style={{ color: "var(--color-fd-muted-foreground)", fontSize: 12 }}>sandbox</span></>) },
  { label: "CAPABILITY", value: (<><ServerChip name="AgentGate" accent /><ServerChip name="Programmable Wallets" /></>) },
]} />

Build a per-agent fund pool that the agent can debit only against a signed mandate. Funding lands via Pix webhook; execute drives the full gateway lifecycle (policy → mandate → wallet hold → route → execute → settle); audit trail renders inline in the dashboard.

This is **AgentGate** territory — managed-tier capability. Self-host customers get the OSS reference wallet; the multi-tenant ledger + recon engine ship in the managed tier.

<PixFlow
  steps={[
    { label: "Create wallet + bind funding", tool: "session.execute(\"create_wallet\")", description: "BRL spend pool + Asaas connection bound as the funding source", iconKey: "card" },
    { label: "Fund via sandbox Pix", tool: "Asaas webhook", description: "Pix lands → webhook normalized → wallet balance posts a credit", iconKey: "qr" },
    { label: "Execute mandate-gated payment", tool: "codespar_pay (gated)", description: "Policy → mandate → wallet hold → route → execute → settle", iconKey: "doc" },
    { label: "Audit trail renders", tool: "Dashboard", description: "hold + release + debit triplet on the ledger; every gate visible", iconKey: "search" },
  ]}
/>

## What you'll build

By the end:

- A BRL wallet that holds the agent's spend pool
- An Asaas connection bound as the wallet's BRL funding source
- A successful mandate-gated execute that posts a `hold` + `release` + `debit` triplet to the ledger
- A visible audit trail showing every gate that fired

## Prerequisites

- A CodeSpar account with at least one [project](/docs/concepts/projects)
- A CodeSpar API key (admin role for the execute endpoint)
- An [Asaas sandbox account](https://sandbox.asaas.com/) with API key
- A signed mandate id from your [mandate generator](/docs/concepts/wallets#execute-lifecycle) — see your AgentGate dashboard

## 1. Connect Asaas

In the dashboard at `/dashboard/connections`, connect Asaas with your sandbox API key. Once connected, copy the `connection_id` (`ca_…`) — you'll bind it to the wallet next.

```bash
# Or via the API
curl -X POST https://api.codespar.dev/v1/connections \
  -H "authorization: Bearer $CODESPAR_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "server_id": "asaas",
    "auth_type": "api_key",
    "credentials": { "access_token": "$AAAS_SANDBOX_KEY" }
  }'
```

## 2. Create the wallet

```bash
curl -X POST https://api.codespar.dev/v1/wallets \
  -H "authorization: Bearer $CODESPAR_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "display_name": "Customer Service Agent",
    "currency": "BRL"
  }'
```

Save the returned `wlt_…` id. The wallet is created with status `active` and a zero-balance row in BRL.

## 3. Bind Asaas as a funding source

```bash
curl -X POST https://api.codespar.dev/v1/wallets/wlt_xyz/funding-sources \
  -H "authorization: Bearer $CODESPAR_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "connection_id": "ca_xyz",
    "currency": "BRL"
  }'
```

From this point on, **any** `commerce.payment.succeeded` event Asaas sends for this connection auto-credits the wallet. The funding bridge is provider-agnostic — same code path covers Stripe, Mercado Pago, Zoop, UnblockPay, and BRLA Digital.

<Callout type="info">
At most one binding per `(connection_id, currency)` while `enabled=true`. Prevents the recon engine from double-applying the same receipt.
</Callout>

## 4. Trigger a sandbox Pix charge

In the Asaas sandbox UI, create a Pix charge against the connected account. As soon as the customer "pays" the sandbox QR (Asaas auto-confirms in sandbox), the webhook fires.

The bridge:

1. Verifies the signature
2. Normalizes the event to `commerce.payment.succeeded`
3. Posts a `kind=fund` ledger entry on the bound wallet
4. Updates `wallet_balances` — both `balance_minor` and `available_minor` go up

Check the wallet:

```bash
curl https://api.codespar.dev/v1/wallets/wlt_xyz \
  -H "authorization: Bearer $CODESPAR_API_KEY"
```

Look for the `balances` array — you should see the BRL row with the funded amount in `balance_minor` (centavos).

## 5. Generate a mandate

A mandate is a HMAC-signed authorization with a cap and expiry. The wallet refuses any debit without a valid mandate id. In your AgentGate environment:

```ts
// MandateGenerator ships with the managed-tier backend; the import
// path is operator-internal. Self-host customers swap in the OSS
// reference mandate from @codespar/sdk's MandateAPI.
import { MandateGenerator } from "@codespar-enterprise/mandate";

const mandates = new MandateGenerator(process.env.MANDATE_SECRET!);
const mandate = mandates.create({
  type: "payment",
  authorizedBy: "user_admin_42",
  agentId: "agt_customer_service",
  amount: 100, // major units, BRL
  currency: "BRL",
  description: "Sandbox demo",
  conditions: [],
  expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
});
console.log(mandate.id); // mnd_…
```

## 6. Execute a mandate-gated payment

```bash
curl -X POST https://api.codespar.dev/v1/wallets/wlt_xyz/execute \
  -H "authorization: Bearer $CODESPAR_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "amount": 50.00,
    "currency": "BRL",
    "recipient": "Acme Supplier Co.",
    "description": "Sandbox payout",
    "mandate_id": "mnd_xyz",
    "preferred_method": "pix"
  }'
```

The gateway runs:

1. **policy_check** — agent budget, rate, time, deny-list
2. **mandate_check** — HMAC verify, cap check, expiry
3. **wallet_hold** — reserves `available_minor` (FOR UPDATE locked)
4. **route_select** — picks Asaas (cheapest BRL Pix route)
5. **payment_execution** — Asaas Pix transfer
6. **wallet_settle** — commits the hold (release + debit + optional fee)
7. **budget_record** — increments the agent's budget counter

The response carries the full `GatewayPaymentResult` including the audit trail and the wallet entry ids.

## 7. Watch the dashboard

Open `/dashboard/wallets/{walletId}`:

- **Hero card** shows the new balance (down by the executed amount)
- **Ledger table** shows the `fund` (from step 4) + `hold` + `release` + `debit` triplet (from step 6) — append-only, double-entry
- **Recon panel** auto-collapses on a clean wallet — the engine will match the debit to the corresponding event within 60s and stamp `reconciled_at`

Click any execute again — the modal renders the **full audit trail inline**, every gate (policy / mandate / wallet_hold / route / execute / wallet_settle) with its pass/fail status and the detail string. This is the primary differentiation surface against Stripe Issuing or Brex when walking a CFO through a denied transaction.

## What's next

- **Multi-rail funding** — repeat steps 1-3 with Stripe (USD/BRL card), Mercado Pago, or [UnblockPay (USDC)](/docs/concepts/wallets#funding-rails). Each rail lands on the same ledger.
- **Insufficient funds path** — execute amount > available; gateway returns `402 requires-approval`. Top up via another funding event to retry.
- **Failure path** — execute against a frozen wallet returns `409 wallet_not_active`. Execute without a mandate id returns `403 denied` with the mandate audit step explaining why.
- **Reconciliation triage** — when a debit's webhook never arrives, the recon engine flags it. Resolve / Dismiss inline in the dashboard panel.

## Production checklist

Before flipping the wallet from sandbox to production:

- ✅ Mandate secret rotated (`MANDATE_SECRET` env var, separate from `CODESPAR_SERVICE_KEY`)
- ✅ Asaas connection switched from sandbox to live API key
- ✅ Wallet hold sweeper enabled (default; opt-out via `WALLET_SWEEPER=off`)
- ✅ Recon engine enabled (default; opt-out via `WALLET_RECON=off`)
- ✅ `WALLET_RECON_PROVIDER_FETCH=on` if you want the engine to fall back to provider API queries when webhooks fail (default: off)
- ✅ Operator drilled on the recon panel triage flow

## See also

- [Wallets concept](/docs/concepts/wallets) — invariants + lifecycle
- [Wallets API](/docs/api/wallets) — REST reference
- [Authentication](/docs/concepts/authentication) — bearer vs service auth
