Agent with a Wallet
Walkthrough of Programmable Wallets: create a wallet, bind an Asaas funding source, fund via sandbox Pix, and see what the mandate-gated execute path does today. ~15 minutes.
Build a per-agent fund pool that the agent can debit only against a signed mandate. Funding lands via Pix webhook, and the ledger and audit trail render inline in the dashboard. The execute step is written to run the full gateway lifecycle (policy → mandate → wallet hold → route → execute → settle), but its mandate stage does not resolve mandates yet: see the callout at step 5 before you plan around it.
This is Guardrails territory — managed-tier capability. Self-host customers get the OSS reference wallet; the multi-tenant ledger + recon engine ship in the managed tier.
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 funded ledger you can read back entry by entry
- A visible audit trail showing every gate that fired, including the mandate gate that refuses the execute today
Prerequisites
- A CodeSpar account with at least one project
- A CodeSpar API key (admin role for the execute endpoint)
- An Asaas sandbox account with API key
- A signed mandate id from your mandate generator. Read the callout at step 5 first: the execute endpoint cannot resolve one yet
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.
# 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
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
Binding a funding source is administrative: an admin or owner does it from the dashboard (the wallet's Funding sources). An API key gets 403 admin_session_required here, whatever its scopes. The request the dashboard makes:
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, the USDC onramp rail, and BRLA Digital.
At most one binding per (connection_id, currency) while enabled=true. Prevents the recon engine from double-applying the same receipt.
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:
- Verifies the signature
- Normalizes the event to
commerce.payment.succeeded - Posts a
kind=fundledger entry on the bound wallet - Updates
wallet_balances— bothbalance_minorandavailable_minorgo up
Check the wallet:
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 managed-tier environment:
// 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_…This id will not be accepted by step 6, and the two steps do not connect today. A MandateGenerator built this way keeps its mandates in the memory of your process. The API's payment gateway builds its own generator with its own in-process store, which starts empty and which no API route writes to, so the id you print here is not a mandate the API can see. Step 6 comes back 403 with "mandate": { "valid": false, "reason": "Mandate not found" }. The buyer-side path that spends against a mandate the API actually stores is POST /v1/consumers/mandates/:id/spend. Steps 1 through 4 and the ledger behaviour in step 7 are unaffected.
6. Execute a mandate-gated payment
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 is written to run these stages in order:
- policy_check — agent budget, rate, time, deny-list
- mandate_check — signature verify, cap check, expiry
- wallet_hold — reserves
available_minor(FOR UPDATE locked) - route_select — picks Asaas (cheapest BRL Pix route)
- payment_execution — Asaas Pix transfer
- wallet_settle — commits the hold (release + debit + optional fee)
- budget_record — increments the agent's budget counter
The response carries the full GatewayPaymentResult including the audit trail and the wallet entry ids. As of today the run stops at stage 2 for every caller, for the reason in the step 5 callout: the mandate store the check reads is empty. The audit array in the response shows exactly that, with mandate_check failed and no wallet_hold entry.
7. Watch the dashboard
Open /dashboard/wallets/{walletId}:
- Hero card shows the balance from step 4's funding entry
- Ledger table is append-only and double-entry. It shows the
fundrow from step 4. Thehold+release+debittriplet is what a completed execute posts, so it appears only once an execute gets past the mandate gate - Recon panel auto-collapses on a clean wallet. When a debit exists, the engine matches it to the corresponding event within 60s and stamps
reconciled_at
Click any execute — 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 the USDC onramp rail. 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 returns403 denied, with a failedwallet_holdaudit step reading "walletId provided without mandateId, wallet debits are mandate-gated". - 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_SECRETenv var, separate fromCODESPAR_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=onif 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 — invariants + lifecycle
- Wallets API — REST reference
- Authentication — bearer vs service auth
Pay Documents and Batches
Turn boleto PDFs, Pix QR images, Pix keys and CSV or XLSX lists into one batch, review what the rail resolved, confirm the exact list and pay it, with a result per item. Same contract over the API, the TypeScript and Python SDKs, the CLI and the `@codespar/mcp` server.
Streaming Chat Agent
Stream an agent turn to a browser: session.sendStream() behind a Next.js route handler, the six event types re-emitted as SSE, and a reader with no extra dependency.