Move from a spend cap to a warrant
A cap limits how much goes wrong. A warrant limits whether it goes wrong at all.
A spend cap bounds your losses after the fact. A warrant holds the money until the deliverable is checked, so the loss does not happen.
cap: pay → hope → discover → dispute → maybe recover
warrant: hold → check → release, part-release, or refundWhat a cap actually gives you
Most agent platforms offer a monthly ceiling. It is worth having and it answers exactly one question: how bad can this month get?
It does not answer whether any individual payment should have been made. Money leaves on request, and everything after that is reconciliation — noticing, then arguing.
What changes
| Spend cap | Warrant | |
|---|---|---|
| When money leaves | On request | After the check passes |
| Bad deliverable | Already paid | Never left held |
| Recourse | Dispute, invoice, chargeback | Refund, instantly |
| Partial value | All or nothing | released_bps |
| Record | Your logs | A trace the payee can read |
Keep the cap as well
Warrants do not replace ceilings — sessions give you both, and the cap still does its job:
{
"amount_ceiling": "50000000",
"warrant_limit": 20,
"failure_budget": 3,
"expires_at": "2026-08-16T12:00:00.000Z"
}The ceiling is cumulative across the session. failure_budget is the one a cap
never gave you: three consecutive failures revoke the session, and a pattern
across sessions pauses the agent. A cap lets an agent fail identically nine
hundred times until the number runs out.
Migrating one call
Before — pay, then hope:
await pay(supplier, "8.40");
const data = await fetch(sourceUrl).then((r) => r.json());
// If data is stale, the money is already gone.After — hold, then check:
import { Warrant, usdc } from "@gowarrant/sdk";
const warrant = new Warrant();
const w = await warrant.open({
amount: usdc("8.40"),
payee: supplier,
spec: {
clauses: [
{ kind: "http.status", equals: 200 },
{ kind: "data.freshness", maxAgeSeconds: 60 },
],
},
deadline: new Date(Date.now() + 3_600_000),
});
const after = await warrant.verify(w.id, { source_url: sourceUrl });
// released | partially_released | escalated | refundedThe spec is the part that carries thought. "What would make this payment wrong?" is a question a cap never made you answer, and the clauses are the answer written down.
Start strict
Use the default policy — only a clean pass pays, everything else escalates. Watch the queue for a week. The failures that turn out to be routine become bands; the ones that turn out to be real are the ones the cap was silently paying for.
Amounts
The one mechanical difference. Amounts are integer strings of the smallest unit,
^[0-9]+$ — not floats, not decimals.
"8400000" 8.40 USDC
8400000 rejected — a JSON number
"8.40" rejected — not the smallest unitusdc("8.40") in the SDK does the conversion and will not compile if you hand
it a number.