Skip to main content

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.

4 min read · updated
View MarkdownEdit on GitHub
TIME
~15 min
PROVIDER
Asaassandbox
CAPABILITY
F2.M4Programmable 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.

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

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:

  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:

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:

  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). 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

Edit on GitHub

Last updated on