An allowance bound to a person, not an address.
We do not issue Proof of Human. We enforce spending rules for a unique human that World already attested (Achieving Proof of Human Stage 3 — authentication / relying party).
Credit cards bind to a card. ERC-20 allowances bind to an address. Revoke either and the counterparty spins up a new credential.
HumanMandate binds authority to a World AgentBook humanId and enforces it in Solidity:
- The rolling 24h cap belongs to the person — two of their agents share one budget
- Revoke cuts off the person — a brand-new agent address still reverts
- Raising the cap or moving the payee requires a fresh Selfie Check. Routine spend inside the cap does not
- Spending can be converted through Uniswap without widening what may be spent
Built at ETHGlobal Lisbon 2026.
Every branch that ends in a refusal is a real mainnet transaction, linked further down.
flowchart LR
A["Any address<br/>they operate"] --> G{"Who is<br/>behind it?"}
K[("World AgentBook<br/>read on-chain")] -.-> G
G -->|"nobody"| R1["NotHumanBacked"]
G -->|"the authorised<br/>person"| C{"Inside the<br/>cap?"}
C -->|"no"| R2["CapExceeded"]
C -->|"yes"| P["Paid — only to<br/>the one locked payee"]
classDef q fill:#ffffff,stroke:#2d2c2c,color:#2d2c2c,stroke-width:1.5px;
classDef no fill:#fdecea,stroke:#b3261e,color:#b3261e,stroke-width:1.5px;
classDef ok fill:#e8f5ee,stroke:#1f7a4d,color:#1f7a4d,stroke-width:1.5px;
classDef n fill:#f9f9f8,stroke:#8b8987,color:#2d2c2c;
class G,C q;
class R1,R2 no;
class P ok;
class A,K n;
Revoking cuts off the person. They come back tomorrow on a brand-new address and enter at the same first gate — and are refused again.
Two properties fall out of this shape:
The budget belongs to the person. Two different addresses operated by the same human draw on one rolling cap, and revoking cuts off every address that person will ever open.
Projects on agentbook.world use AgentKit to gate HTTP endpoints. None we measured use AgentBook inside a contract to guard money. We read the live AgentBook from Solidity and revert on-chain.
| Contract | Address |
|---|---|
| HumanMandate | 0x7fcEc100ADc4e89b09a92e3f7931161791D06054 |
| MandateSwapper | 0x4054fC0708799B906276984575cDfaBbe1Df45e9 |
| AgentBookRegistry | 0x9Ac36746eFbb8192b0D5BB8C0774026bff1b9aB4 |
| World AgentBook (official) | 0xA23aB2712eA7BBa896930544C7d6636a96b944dA |
A revert here is a designed refusal, mined deliberately so it can be verified. Every selector was recovered with cast run and matched against cast sig — not guessed.
| What it proves | Result | Tx |
|---|---|---|
| An address the mandate never named spends anyway | ok | 0xb7fa49a1…4620 |
| A wallet with no human behind it | 0x203ac8ca NotHumanBacked |
0x9ebb088a…1f0e |
| A second agent of the same person shares the budget | ok | 0x7da5b4ba…767e |
| One payment over the per-payment cap | 0xcb0bcbd5 PerTxCapExceeded |
0x33e79b96…c650 |
| One wei past the rolling 24h cap | 0x2e8b3b3b CapExceeded |
0xe91dcc5f…f2be |
| Raising the limit with no Selfie Check | 0x6aaa9349 LivenessRequired |
0x61f5ccf4…cdd5 |
| Raising it with a fresh attestation | ok | 0xb1e64600…e603 |
| Replaying that attestation | 0x6c866211 LivenessAlreadyUsed |
0x1fa5fa90…11dc |
| The revoked person returns on a fresh address | 0xa4e1a97e NotAuthorized |
0x23db1c10…5dab |
Rows one and two are the discriminating pair. Either alone proves nothing; together they prove the registry read is the actual gate.
Where the money is spent. A mandate on its own moves one token to one payee. MandateSwapper lets a mandated agent convert what it is allowed to spend into the asset the payer chose, routed by the Uniswap Trading API, without widening what it may spend.
Developer feedback for this integration: FEEDBACK.md.
The problem this exists to solve. A cap counts what leaves the payer. Put a swap in the middle and an agent can stay under the cap forever while still draining value — route through a bad pool, or sandwich itself. The amount spent looks obedient; the amount received does not. A cap with no floor on the output is not a cap.
The whole path, both contracts:
flowchart LR
A["Any address<br/>the person operates"] --> B{"Human<br/>behind it?"}
B -->|no| R1["NotHumanBacked"]
B -->|yes| C{"The person the<br/>payer authorised?"}
C -->|no| R2["WrongHuman"]
C -->|yes| D{"Within the per-payment<br/>and rolling 24h caps?"}
D -->|no| R3["PerTxCapExceeded<br/>CapExceeded"]
D -->|yes| E["Paid to the payee<br/>fixed at authorisation"]
K[("World AgentBook<br/>lookupHuman()")] -.->|"answers both gates"| B
E --> F["Uniswap Trading API<br/>route"]
F --> G{"Did the payee receive<br/>at least the floor?"}
G -->|no| R4["SlippageTooHigh"]
G -->|yes| H["Payee paid in the<br/>asset the payer chose"]
classDef default fill:#f9f9f8,stroke:#8b8987,color:#2d2c2c,stroke-width:1px;
classDef gate fill:#ffffff,stroke:#2d2c2c,color:#2d2c2c,stroke-width:1.5px;
classDef refuse fill:#fdecea,stroke:#b3261e,color:#b3261e,stroke-width:1.5px;
classDef allow fill:#e8f5ee,stroke:#1f7a4d,color:#1f7a4d,stroke-width:1.5px;
classDef ext fill:#efedea,stroke:#8b8987,color:#2d2c2c,stroke-dasharray:3 3;
class B,C,D,G gate;
class R1,R2,R3,R4 refuse;
class E,H allow;
class K ext;
So the contract measures the actual balance delta and refuses below the floor:
| Where | File | Lines |
|---|---|---|
| Output floor enforced on the measured delta | contracts/src/MandateSwapper.sol |
154–159 |
| Router is immutable (the agent cannot name its own "router") | contracts/src/MandateSwapper.sol |
61 |
| Payer fixes the asset and payee; the agent picks only timing and route | contracts/src/MandateSwapper.sol |
81–92 |
Trading API /quote and /swap calls |
scripts/swap.ts |
21, 95, 143 |
Routing-aware /swap body (UniswapX vs CLASSIC) |
scripts/swap.ts |
67–79 |
| AgentBook read that gates every spend | contracts/src/HumanMandate.sol |
105–111, 210–212 |
Real route from the Trading API (CLASSIC, 526 bytes of calldata), real liquidity, real refusal.
| Step | Result | Tx |
|---|---|---|
| Payer opens the mandate, paying out to the swapper | ok | 0xf8c784f4…3d6e |
| Payer declares what it converts into, and who receives it | ok | 0x64d5c3c2…914f |
| The agent spends 0.0005 WETH, under the cap | ok | 0x92cf187c…8473 |
| Settle demanding a floor the route cannot pay | 0x76baadda SlippageTooHigh |
0x354bd126…4ea1f |
| Settle with an honest floor — payee receives 0.939127 USDC.e | ok | 0x33ad7da0…09a5 |
The refusal is the point. Quoted output was 939042 base units; demanding 1878084 was refused with both figures in the revert data.
The run above spends WETH. The contract does not care which token it is, so the same functions run a second card denominated in real USDC.e — a $1.50 daily cap with a $0.50 ceiling per payment, converting to WETH for the payee. Nothing about the contract changed; the token address is a parameter.
| Step | Result | Tx |
|---|---|---|
| The agent spends 0.50 USDC.e, under the cap | ok | 0x96276665…6219d |
| Settle demanding a floor the route cannot pay | 0x76baadda SlippageTooHigh |
0x392bddb6…9dce85 |
| Settle with an honest floor — payee receives the WETH | ok | 0xaef46ee0…db2820 |
This is the shape a dollar-cost-averaging mandate takes: the agent converts a fixed daily amount and the proceeds land at an address the payer fixed. The agent holds neither side.
Developer feedback for this integration: FEEDBACK.md.
cd contracts
./bootstrap.sh # pinned forge-std + OpenZeppelin, ~11 MB
forge test48/48 passing, including 4 fork tests against the live World Chain AgentBook (pinned block 32845385). The one that matters most:
test_a_bad_route_is_refused_even_though_the_cap_was_respected— the cap is obeyed and the transaction is still refused
Built with Claude Code / Cursor as pair programmers for scaffolding, tests, and doc-driven integration. Architecture, product calls, and verification of every on-chain claim were made by the team. Every transaction hash in this README was executed for real.