
ARCANUM
Governed USDC wallets for AI agents on Arc, now with signed, offline-verifiable payment receipts.
Demo
About
Arcanum is a wallet layer for AI agents that have to spend money. Instead of handing an agent a normal wallet and hoping the prompt holds, each agent gets a GuardedWallet contract on Arc with a policy attached by the owner: how much it can spend per payment, per day and per month, which vendor categories it can pay, whether the destination has to be on an allowlist, and the amount above which a human has to look at it. The contract checks all of that inside the same transaction that moves the USDC. Small routine payments just go through. Anything over the review threshold gets held on chain until someone on the council approves or rejects it. Unknown destinations get denied. There is nothing to bypass because the rule lives in the same place as the money. I have been building this for a while and the app was already live before ETHOnline, so this is a Continuity entry. The repo has a tag marking where the event started, and PRE-EXISTING.md in the root walks through what was there before and what I added during the event, commit by commit. What I added is payment decision receipts. The idea came from a gap I kept running into. The wallet enforces policy and the indexer shows what happened after the fact, but there was no way to prove what the policy said about one specific payment at one specific moment, especially for a deny, which reverts and leaves nothing on chain. So now, before an agent pays, it can ask for a receipt. The API evaluates the exact signed payment intent against the wallet's policy at one pinned block and signs the result. The receipt has the verdict (allow, escalate, deny or freeze), the reason code, the caps and spend counters it looked at, the vendor entry, whether the signer was authorized, the block number and hash, and the agent's own signature over the request. You can verify it offline against a published issuer registry, no account, no API call. There is a public verify page on the site that does it in the browser. The receipt id also goes into the calldata of the transaction that acts on it. Afterwards the API links the transaction back to the receipt as evidence: executed, escalated, reverted, and later the council's outcome. An auditor can put the decision next to what actually happened and see whether the chain agreed. One thing I want to be clear about: a receipt never authorizes anything. The contract has no idea receipts exist and evaluates policy again when the transaction lands. The receipt is a record of a decision, not a permission slip. Everything for this sits on the ethonline-2026 branch: the receipt format and verifier, issuance and evidence linking in the API, the SDK methods (requestPaymentReceipt, executePaymentIntentWithReceipt and attachPaymentReceiptEvidence, and the SDK verifies every receipt before it acts on one), the receipts pages in the dashboard with an evidence timeline, the public verify page, a Circle signer so the agent can sign from a Circle developer-controlled wallet instead of a key file on its own machine, and an inbound CCTP V2 funding flow so a governed wallet on Arc can be topped up from Sepolia USDC.
How it's made
The base is what Arcanum already was: Solidity 0.8.24 contracts built with Foundry (GuardedWallet, WalletFactory, PolicyEngine, VendorRegistry, EscalationManager, AnomalyOracle, all on Arc Testnet), a TypeScript monorepo with a Next.js 15 app, tRPC with SIWE sessions, Supabase for the read models, a Ponder indexer feeding them, viem and wagmi on the client, and a TypeScript and a Python SDK. I did not touch the contracts during the event. Receipts live next to them. The receipt body is a strict schema. Unknown fields are rejected, addresses are lowercased, anything that can exceed 2^53 is a decimal string. It gets serialized with a subset of RFC 8785 (sorted keys, no whitespace, no floats) and hashed with SHA-256, so the digest comes out identical in TypeScript, Python or anything else. The issuer signs that digest with a plain EIP-191 personal signature over a fixed prefix plus the digest. Issuer keys are published in a registry with validity windows, per chain, so verification works offline and I can rotate keys later without breaking old receipts. The API refuses to issue if its configured key is not an active registry entry for the chain it is running on. To issue one, the API reads the wallet, the policy engine, the vendor registry and the escalation manager with normal eth_calls, all at the same pinned block. The annoying part was the spend windows. The contract rolls the daily and monthly counters over based on block timestamp, so the receipt has to do the same math at that block's timestamp or it would show numbers executeUSDC would never see. After the last read it checks the block hash again; if it changed, that is a CHAIN_READ_FAILED, not a wrong receipt. Envelopes are stored in a Supabase table with a trigger that blocks updates and deletes. Issuance is idempotent per intent reference, so asking twice gets you the original back flagged as replayed. Nothing goes on chain to issue a receipt. For evidence, the agent puts the receipt id inside the reason string of executeUSDC. To link, the client only sends a transaction hash. The API pulls the transaction and its logs from the Arc RPC itself, requires exactly one matching event from that wallet, notes whether the calldata actually names the receipt, and appends a row: executed, escalated, frozen or reverted, then later the escalation status, each with a flag for whether the chain agreed with the verdict. A denied executeUSDC reverts and emits nothing, so a deny only exists as a receipt, and a revert is evidenced from the transaction status alone. One transaction can only ever link to one receipt. In the SDK, verifyPaymentReceipt runs four separate checks: format, digest, issuer signature, and the agent's request signature. executePaymentIntentWithReceipt will not submit unless all four pass and the receipt actually answers the intent it just signed. It also will not act on a replayed receipt by default, because the earlier attempt may already have paid and the contract does not dedupe references, so a second executeUSDC is simply a second payment. The dashboard and the public verify page run the exact same verifier in the browser. Verification never talks to the API. The Circle piece is a viem local account whose signMessage, signTransaction and signTypedData get forwarded to a Circle developer-controlled wallet, so the agent's key lives in Circle's MPC setup and never on the agent's box. I treat Circle as a signing oracle, not something to trust: every signature has to recover to the wallet address, a signed transaction gets parsed back and compared field by field against what I asked for, the entity secret gets re-encrypted for every single request because Circle rejects reused ciphertext, and the SDK does gas estimation and broadcasting to Arc on its own. The thing that cost me the most time: the wallet has to be created on Circle's generic EVM-TESTNET identifier. Named chains like ARC-TESTNET do not support transaction signing, and it took a while to work out that this was what the error meant. The governed wallet, the policy and the receipts have no idea Circle is behind the signer. CCTP funding burns native USDC on Sepolia with CCTP V2 and mints straight into the governed wallet on Arc. I request Circle's Forwarding Service for the destination mint and keep a separately approved, gas-capped manual relay for when forwarding does not finish. Settlement is checked strictly against the attestation, the nonce and the MessageReceived log on Arc. One detail worth knowing on Arc: USDC is also the gas token, native balances are 18 decimals while the ERC-20 side is 6, and the funding code has to keep those apart or the amounts are off by a lot. On process: most of the event code, tests, the migration and the docs were written by an AI coding agent working from my written plan, with a second AI reviewing each slice. Design decisions, deployment, the testnet runs and the video are mine. No new npm dependencies were added. It is all written up in PRE-EXISTING.md.