Technical design · Tempo chainId 4217
Architecture
Mandate is a policy engine that sits in front of Tempo TIP-20 transfers. Agents never hold treasury keys. Every paid HTTP request is an MPP challenge whose on-chain memo is the challenge id. This page is the design the Solidity in contracts/ implements 1:1 with the in-browser VM.
Control plane
Finance (owner) deposits pathUSD into MandateBook. Issuing a mandate creates a spend policy: spender, cap, validity window, hourly rate, department code, merchant allowlist. Empty allowlist is fail-closed. The spender key — typically an agent session key — may only call pay(id, merchant, amount, memo).
CompanyTreasury (TIP-20 pathUSD)
│
▼
MandateBook
├── Mandate #n
│ spender, spendMax, remaining
│ validAfter / validUntil
│ rateLimit / window
│ allowlist[] // empty = deny all
│ paused / closed
└── pay()
checks policy
transferWithMemo(merchant, amount, challengeId)
feePayer = finance (Tempo type 0x76)Why these Tempo primitives
- TIP-20, 6 decimals. pathUSD at 0x20c000…000000. Memos are 32 bytes — we store the MPP challenge id so finance reconciliation is a join, not a guess.
- Stablecoin gas. Fees in attodollars, paid in the same TIP-20. A pay() is ~84k gas ≈ $0.001 at the cap. Finance sponsors via
feePayerso the agent hot wallet can be empty. - Payment lanes. TIP-20 transfers ride reserved blockspace. Agent micropayments do not compete with general compute during congestion.
- 0.6s finality. MPP is a synchronous HTTP cycle. The merchant can wait for inclusion before returning 200.
- Type 0x76. Native batching, concurrent nonces, passkeys, scheduled txs — no 4337 bundler.
MPP flow
- Agent GET resource, no credential.
- Merchant 402 +
WWW-Authenticate: Payment method="tempo" ... - Agent calls MandateBook.pay with
memo = bytes32(challengeId). - On success, retry with
Authorization: Paymentcarrying the tx hash. - Merchant verifies transfer + memo, returns 200 and Payment-Receipt.
- On policy miss, pay() reverts a typed error. Merchant never sees funds.
Custom errors
Typed reverts are part of the UX. The console renders them next to the HTTP trace so finance can see why an agent was stopped — MerchantNotAllowed, CapExceeded, RateLimited, MandatePaused, MandateExpired, NotSpender, InsufficientTreasury, ZeroAmount.
Security notes
- Fail-closed allowlist. Unknown merchants cannot drain a cap.
- Spender is not owner. Compromised agent key cannot withdraw treasury.
- Rate window resets only after
windowseconds, not on every pay. - pause() is a one-transaction freeze. close() is terminal.
- Reentrancy guard around pay() before the TIP-20 transfer.
- This demo runs an isomorphic VM in the browser so judges can click the failure path without a wallet. Production deploy: Foundry script against Tempo mainnet/testnet, pathUSD as token.
Protocol addresses
- TIP-20 Factory
- 0x20Fc000000000000000000000000000000000000
- pathUSD
- 0x20c0000000000000000000000000000000000000
- USDC
- 0x20c0000000000000000000000000000000000001
- Fee Manager
- 0xfeec000000000000000000000000000000000000
- Stablecoin DEX
- 0xdec0000000000000000000000000000000000000
- TIP-403 registry
- 0x403c000000000000000000000000000000000000