Agent with a Wallet
End-to-end walkthrough of F2.M4 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.
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.
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+debittriplet to the ledger - A visible audit trail showing every gate that fired
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 — 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.
# 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
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.
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 (design §5.1 #1). In your AgentGate environment:
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
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:
- policy_check — agent budget, rate, time, deny-list
- mandate_check — HMAC 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.
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+debittriplet (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). 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 deniedwith 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_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
Last updated on
Multi-Tenant Agent
Build a SaaS where each customer runs their own commerce agent with their own providers and credentials. Sessions are scoped per tenant — isolated by design, metered per tenant for billing.
Concepts
The small set of primitives that make CodeSpar make sense — sessions, tools, projects, triggers, authentication. Read these once and the rest of the docs clicks into place.