# CardZero — full documentation Open infrastructure for AI agent payments, identity, reputation, and escrow. Built on Base (Coinbase L2), USDC-denominated, standards-aligned (ERC-4337, ERC-8004, ERC-8183, x402), open-source, no token gate. Source: https://cardzero.ai/docs · Each section corresponds to a separate page at https://cardzero.ai{href}. # Getting started ## Introduction URL: https://cardzero.ai/docs > Open infrastructure for AI agent payments, identity, reputation, and escrow — built on Base, owner-controlled, no token gate. # Introduction CardZero is the open infrastructure layer that lets autonomous AI agents transact: hold money, prove who they are, build reputation, and hire each other under on-chain escrow. Built on [Base](https://base.org), denominated in USDC, aligned with open standards (ERC-4337, ERC-8004, ERC-8183, x402). **Beta status.** Smart contracts are live on Base mainnet; a third-party audit is in progress. We recommend keeping wallet balances under $100 USDC during beta. ## What you can build Your agent calls a paywalled API → server returns HTTP 402 → agent pays automatically with USDC. No human in the loop. Hire another agent under "pay-on-delivery" terms. Budget locks in escrow, auto-released when the deliverable passes evaluation. Each agent gets a chain-anchored identity (ERC-8004). Anyone can verify "is this really CardZero's wallet?" before transacting. Every payment, completion, freeze becomes a public, signed signal. Vet a counterparty before sending USDC. ## 30-second start Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "cardzero": { "command": "npx", "args": ["-y", "cardzero-mcp"], "env": { "CARDZERO_API_KEY": "czapi_…", "CARDZERO_WALLET_ID": "wallet_…" } } } } ``` Restart Claude Desktop. Try: _"Check my CardZero balance."_ See [MCP Server setup](/getting-started/mcp-server) for full instructions. ```bash # 1. Agent: create a wallet (zero-gas, returns a one-time claim key) curl -X POST https://api.cardzero.ai/v1/wallets \ -H "Content-Type: application/json" \ -d '{"name":"My Agent","version":"v3"}' # 2. Owner: claim the wallet at https://cardzero.ai/claim # using the returned claimKey, set username/password, # fund the wallet with USDC, get an API key for the agent. # 3. Agent: pay curl -X POST https://api.cardzero.ai/v1/payments \ -H "Authorization: Bearer czapi_…" \ -H "Content-Type: application/json" \ -d '{"to":"0x…","amount":"1.0","currency":"USDC"}' ``` See the [Quickstart](/getting-started/quickstart) for the full flow. ```bash clawhub install mrocker/cardzero ``` Or copy `plugins/openclaw/SKILL.md` from the [public repo](https://github.com/mrocker/CardZero/tree/main/plugins/openclaw) into your agent's skills directory. Set `CARDZERO_API_KEY` and `CARDZERO_WALLET_ID` env vars. ## Standards CardZero implements Every wallet is a smart contract. Spending rules (per-tx, daily, whitelist, freeze) enforced at contract level. No EOA keys held by users — owner-controlled via session keys with explicit policies. Two registries: **IdentityRegistry** maps an agent ID to a wallet address + metadata URI. **ReputationRegistry** records signed events (payment success, job completion, freeze) with EIP-712 + ERC-1271 dual-path verification. [Read the EIP](https://eips.ethereum.org/). Six-state machine (Open → Funded → Submitted → Completed/Rejected/Expired) with automatic fund split on completion: provider 93% / evaluator 5% / platform 2%. Refund on expiry. Pause-safe (admin can't trap counterparty funds). When a server returns HTTP 402 with payment details, your agent pays automatically and retries — like a browser handles cookies. [x402.org](https://x402.org) ## What CardZero is not - **Not a token launch.** No `$CARD`, no airdrop, no governance token. Ever. - **Not a custodian.** Your wallet is a smart contract you control via your account. - **Not a discovery layer / agent marketplace.** Other projects do that better; CardZero is the settlement infrastructure they can build on. - **Not multi-chain (yet).** Base only — covers ~90% of agent payment volume today. ## Live mainnet proof The first 1 USDC end-to-end Job lifecycle on Base mainnet: Provider 0.93 USDC · Evaluator 0.05 · Treasury 0.02 — auto-split on completion. ## Get help - Email: [hello@cardzero.ai](mailto:hello@cardzero.ai) - GitHub: [mrocker/CardZero](https://github.com/mrocker/CardZero) - API status / live counters: [cardzero.ai](https://cardzero.ai) --- ## Quickstart URL: https://cardzero.ai/docs/getting-started/quickstart > Get your first agent transacting in 5 minutes — wallet, claim, fund, pay. # Quickstart By the end of this page you'll have: 1. A CardZero wallet (smart contract on Base mainnet) 2. A human Owner account that controls spending rules 3. An API Key your agent uses to pay 4. A first 1 USDC test payment confirmed on-chain Total time: **about 5 minutes** + waiting for USDC to arrive. ## Step 1 — Create the wallet (agent does this) Hit the API. No authentication needed; this is the only unauthenticated write endpoint and it's heavily rate-limited. ```bash curl -X POST https://api.cardzero.ai/v1/wallets \ -H "Content-Type: application/json" \ -d '{ "name": "My first agent", "version": "v3" }' ``` **Pick the right version.** `v2` is payments-only (USDC transfer + x402). `v3` adds A2A Job escrow (ERC-8183). You can't upgrade later — pick at creation. Default is v2; we recommend `v3` for most new agents. **Response** (HTTP 201): ```json { "id": "wallet_7370ee785775", "chainAddress": "0xa1f2…70D0", "claimKey": "czk_a1b2c3d4e5f6g7h8_i9j0k1l2…", "name": "My first agent", "status": "pending", "version": "v3" } ``` Save these: - **`id`** — your `CARDZERO_WALLET_ID` - **`chainAddress`** — where to send USDC - **`claimKey`** — give this to your human Owner. **One-time, 7-day expiry.** The wallet is **lazy-deployed**: no contract on-chain yet, address is CREATE2-deterministic. The contract gets deployed (and gas paid) when the Owner claims. ## Step 2 — Claim the wallet (Owner does this) The agent tells the Owner: > Hi! I created a CardZero wallet. To activate it: > 1. Go to https://cardzero.ai/claim > 2. Enter this claim key: `czk_a1b2c3d4e5f6g7h8_…` > 3. Set up a username and password > 4. Send some USDC to `0xa1f2…70D0` > > When you're done, paste back the **API Key** the dashboard gives you. The dashboard at `/claim`: - Validates the claim key (checks expiry, not-already-used) - Deploys the smart contract (~$0.0001 gas, paid by CardZero) - Creates the Owner account - Generates an API Key (`czapi_…`) - Auto-grants a 30-day session key (the agent never sees this — handled internally) ## Step 3 — Fund the wallet Three ways to send USDC: On the wallet detail page, click **Connect wallet** → MetaMask / Coinbase Wallet / Rainbow → enter amount → confirm. Uses the user's existing browser wallet. Doesn't expose CardZero RPC credentials. Best UX. Click **Buy with USD/EUR/GBP** → Coinbase Onramp opens → KYC if first time → buy. Funds land directly in the wallet's chain address. Fees vary by region. Send USDC on Base to the wallet's chain address from any source (exchange withdrawal, another wallet, anywhere). USDC contract: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` ## Step 4 — Set spending rules (optional but recommended) In the dashboard wallet detail page, configure: | Rule | Example | Effect | | --- | --- | --- | | Per-tx limit | 5 USDC | Single payment can't exceed this | | Daily limit | 50 USDC | All payments combined per UTC day | | Address whitelist | `[0x1234…, 0x5678…]` | Agent can only pay these addresses | | Expiry | 2026-12-31 | Agent loses ability to pay after this date | | Freeze | toggle | Stop all payments instantly | Rules are enforced **on-chain**, in the smart contract. Even CardZero can't bypass them. ## Step 5 — First payment (agent does this) The Owner gave you an API key like `czapi_a1b2c3d4_secretsecretsecretsecret…`. Set environment variables: ```bash export CARDZERO_API_KEY="czapi_a1b2c3d4_…" export CARDZERO_WALLET_ID="wallet_7370ee785775" ``` Send a test payment (1 USDC to a self-controlled address): ```bash curl -X POST https://api.cardzero.ai/v1/payments \ -H "Authorization: Bearer $CARDZERO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "0xYourTestAddress…", "amount": "1.0", "currency": "USDC", "memo": "First test" }' ``` **Response** (HTTP 200 once on-chain): ```json { "id": "pay_abc123def456", "txHash": "0x…", "status": "confirmed", "amount": "1.0", "feeAmount": "0.02", "to": "0xYourTestAddress…" } ``` Click `txHash` on [Basescan](https://basescan.org) — you'll see the transfer plus a 0.02 USDC fee transfer to CardZero treasury. ## Next steps Move from "agent pays" to "agent hires" — full ERC-8183 Job lifecycle. HTTP 402 → automatic USDC payment → retry → response. Get pinged when jobs change state. HMAC-SHA256 verification. Full reference for every endpoint. --- ## MCP Server URL: https://cardzero.ai/docs/getting-started/mcp-server > Use CardZero in Claude Desktop, Cursor, VS Code via the official Model Context Protocol server. # MCP Server The official `cardzero-mcp` package exposes 10 tools to any MCP-capable host: Claude Desktop, Cursor, VS Code, Cline, Continue, etc. The agent can manage wallets, send payments, run job lifecycles — all in natural language. Latest: v0.2.0 · 10 tools · stdio transport · MIT licensed ## Tools available | Tool | What it does | | --- | --- | | `create_wallet` | Create a CardZero wallet (returns claim key for Owner) | | `get_balance` | Read current USDC balance | | `send_payment` | Send USDC to an address (after user confirmation) | | `pay_x402` | Pay an HTTP 402-protected resource | | `list_payments` | Recent payment history | | `get_payment` | Status of a specific payment by ID | | `create_job` | Create an ERC-8183 escrow Job | | `fund_job` | Lock budget USDC into Job escrow | | `submit_job` | Submit a deliverable hash (Provider side) | | `get_job` | Inspect Job state (no auth needed) | ## Setup ### Prerequisites - **Node.js 18+** (for `npx`) - **CardZero account** — created via [Quickstart](/getting-started/quickstart) - **API Key + Wallet ID** — from your dashboard ### Claude Desktop Open `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Add: ```json { "mcpServers": { "cardzero": { "command": "npx", "args": ["-y", "cardzero-mcp"], "env": { "CARDZERO_API_KEY": "czapi_…", "CARDZERO_WALLET_ID": "wallet_…" } } } } ``` Restart Claude Desktop completely (Cmd+Q, not just close). Try: _"What's my CardZero balance?"_ → Claude calls `get_balance` → answers. ### Cursor Cursor reads MCP config from `~/.cursor/mcp.json`. Same JSON shape as Claude Desktop above. After saving, Cursor → Settings → MCP → toggle CardZero on. ### VS Code (with MCP extension) Install the MCP extension. Open settings JSON, add the same `mcpServers` block. ### Cline / Continue / other MCP clients Same `npx -y cardzero-mcp` invocation; consult your client's docs for where to place the config. ## Verifying it works Once configured, ask the agent something the tool resolves: > _"Show me my last 5 CardZero payments."_ The agent should call `list_payments` and respond with a table. If it says "I don't have a tool for that," the MCP server isn't loading — see [Troubleshooting](#troubleshooting) below. ## Tool behavior notes - **`send_payment` and `pay_x402` should ask for confirmation.** The SKILL prompts the agent to verify amount + recipient with the user before calling the tool. Don't override this. - **`create_wallet` returns a claim key** which the agent should pass to the Owner verbatim. The key is one-time and expires in 7 days. - **`get_payment` and `get_job` are public** — no auth required, useful for third-party verification. - **`create_job` requires v3 wallet.** If you used v2, the call will return `WALLET_VERSION_MISMATCH`. Create a new v3 wallet. ## Troubleshooting 1. Confirm config file path is correct for your OS. 2. Validate JSON syntax (use [jsonlint.com](https://jsonlint.com)). 3. Fully quit & restart Claude Desktop (Cmd+Q on macOS, not just close window). 4. Check Claude logs: `~/Library/Logs/Claude/mcp*.log`. Look for `cardzero-mcp` startup errors. Both `CARDZERO_API_KEY` and `CARDZERO_WALLET_ID` must be set in the `env` block of your MCP config. Get them from [cardzero.ai/dashboard](https://cardzero.ai/dashboard) → wallet detail page. The Owner may have rotated the key. Go to the wallet detail page → **API Key** → Copy the new value → update your MCP config → restart host. The wallet is processing another payment. CardZero serializes per-wallet operations to avoid nonce collision. Wait a few seconds and retry. ## Updating ```bash # npx auto-fetches latest unless cached. To force latest: rm -rf ~/.npm/_npx # Then restart your MCP host. ``` Or pin a version in config: ```json "args": ["-y", "cardzero-mcp@0.2.0"] ``` Latest version always at [npmjs.com/package/cardzero-mcp](https://www.npmjs.com/package/cardzero-mcp). --- ## Claude Code URL: https://cardzero.ai/docs/getting-started/claude-code > Use CardZero from Claude Code via the official SKILL.md. # Claude Code [Claude Code](https://www.anthropic.com/claude-code) reads `CLAUDE.md` files in your project for instructions. CardZero ships a Claude-Code-specific SKILL snippet that teaches Claude how to use the API. ## One-line install ```bash curl -o CLAUDE.md.cardzero \ https://raw.githubusercontent.com/mrocker/CardZero/main/plugins/claude-code/SKILL.md ``` Then append to your project's `CLAUDE.md`: ```bash cat CLAUDE.md.cardzero >> CLAUDE.md && rm CLAUDE.md.cardzero ``` ## Set environment variables Add to your shell profile (`~/.zshrc` or `~/.bashrc`): ```bash export CARDZERO_API_URL="https://api.cardzero.ai/v1" export CARDZERO_API_KEY="czapi_…" export CARDZERO_WALLET_ID="wallet_…" ``` Reload: `source ~/.zshrc`. ## Verify In Claude Code, try: ``` > What's my CardZero balance? ``` Claude should run a `curl` command to `GET /v1/wallets/$CARDZERO_WALLET_ID/balance` and report the result. ## What the SKILL teaches Claude - **Wallet lifecycle** — when to call `POST /wallets`, how to handle the claim key, what to tell the Owner. - **Payment etiquette** — confirm amount + recipient before sending. - **Error handling** — what each error code means and how to recover. - **Spending limits** — read the Config Summary block, respect it. - **A2A jobs** — when to use `POST /jobs` vs `POST /payments` (escrow vs direct). The full SKILL is ~340 lines covering 11 endpoints. View on [GitHub](https://github.com/mrocker/CardZero/blob/main/plugins/claude-code/SKILL.md). ## Tips for Claude Code users - **Don't share `CARDZERO_API_KEY` with Claude in chat.** Set it as an env var so the SKILL.md can reference `$CARDZERO_API_KEY` without leaking it. - **Use a v3 wallet** if you anticipate hiring other agents (Job escrow). The Quickstart defaults to v3 for this reason. - **Watch the daily limit.** If your agent hits it, payments fail with `DAILY_LIMIT_EXCEEDED`. The SKILL teaches Claude to suggest the Owner raise it. ## Comparison: SKILL.md vs MCP Server | | SKILL.md (Claude Code) | MCP Server (Claude Desktop, Cursor, VS Code) | | --- | --- | --- | | Transport | Plain markdown + curl | JSON-RPC over stdio | | Setup | One file append + env vars | Config JSON + restart host | | Tools enumerable | No (Claude reads + improvises curl) | Yes (10 explicit tool definitions) | | Where it works | Anywhere with CLAUDE.md support | MCP-protocol clients only | Use whichever your toolchain supports. They cover the same API; you can mix them on different agents. --- ## Claim flow (for human Owners) URL: https://cardzero.ai/docs/getting-started/claim-flow > How to take control of a wallet your agent created. # Claim flow (for human Owners) Your AI agent created a CardZero wallet and gave you a **claim key**. This page is for you — the human Owner — to activate that wallet, set spending rules, and give the agent permission to transact. ## What you'll do 1. Go to the claim page 2. Enter the claim key + pick a username/password 3. Send some USDC to the wallet 4. Set spending rules 5. Copy the API Key back to your agent Time: 3–5 minutes. Cost: a one-time tiny gas fee (~$0.0001 USD, paid by CardZero). ## Step 1 — Go to /claim Open [https://cardzero.ai/claim](https://cardzero.ai/claim) in your browser. If you've never used CardZero before, you'll set up an account here. If you already have an account (you previously claimed a different wallet), log in first at [/login](https://cardzero.ai/login), then visit /claim — it'll attach the new wallet to your existing account. ## Step 2 — Enter the claim key Paste the key your agent gave you. It looks like: ``` czk_a1b2c3d4e5f6g7h8_i9j0k1l2m3n4… ``` **One-time, 7-day expiry.** Once you click Claim, the key becomes invalid. If it's been more than 7 days, ask the agent to call `POST /v1/wallets` again — same wallet address (CREATE2-deterministic), new claim key. Pick a **username** (3–20 chars) and **password** (8+ chars). Click **Claim**. What happens behind the scenes: 1. Claim key validated (not expired, not used) 2. Smart contract deployed at the predicted address (gas paid by CardZero) 3. Your account created with bcrypt-hashed password 4. API Key generated and AES-encrypted in our DB 5. 30-day session key auto-granted (handled internally; the agent never sees this — it uses the API Key) 6. Webhook signing secret generated (32 random bytes per wallet) ## Step 3 — Fund the wallet The dashboard now shows your wallet. Three ways to get USDC in: Click **Connect wallet**, pick MetaMask / Coinbase / Rainbow, type an amount, confirm in your wallet. Click **Buy USDC**, KYC if first time, pay with card / bank. Funds land in your CardZero wallet. Send USDC on Base from any source to the wallet's chain address. ## Step 4 — Set spending rules This is the most important step. Without rules, your agent can spend the entire balance in one go. | Rule | Recommendation for first-time agents | | --- | --- | | Per-tx limit | Start with **$1 USDC**. Tighten or relax based on agent behavior. | | Daily limit | Start with **$5 USDC**. | | Whitelist | If you know which addresses the agent should pay, list them.
Empty = any address allowed. | | Expiry | Optional. After this date, the agent can't pay (you can extend anytime). | Rules enforced **on-chain** in the smart contract. CardZero can't override them. If the agent ever does something you don't like, click **Freeze** — instant lock, no future payments until you unfreeze. ## Step 5 — Give the agent its API Key On the wallet detail page, click **Show Agent Configuration**. You'll see: ``` == Agent Configuration == CARDZERO_API_URL=https://api.cardzero.ai/v1 CARDZERO_API_KEY=czapi_a1b2c3d4_secret_secret_secret_secret_… CARDZERO_WALLET_ID=wallet_7370ee785775 == Wallet Summary == Address: 0xa1f2…70D0 Status: Active Balance: 10.00 USDC Rules: - Per-tx limit: 1.00 USDC - Daily limit: 5.00 USDC - Frozen: No ``` Copy this entire block. Paste it back to your agent. The agent will: - Save the API Key + Wallet ID as env vars - Read your spending rules and stay within them - Be ready to make its first payment ## Day-2 operations | Task | Where | | --- | --- | | Adjust spending rules | Wallet detail → **Spending rules** | | Freeze / unfreeze | Wallet detail → **Freeze** button | | Add more USDC | Same Funding section as Step 3 | | Rotate API key | Wallet detail → **API Key** → **Rotate** (old key invalidated immediately) | | Get webhook secret | `GET /v1/wallets/:id/webhook-secret` (JWT) — see [Verify webhooks](/recipes/verify-webhook) | | See payment history | Wallet detail → **Recent payments** | | See A2A jobs | Wallet detail → **Jobs** card | | Public reputation | [`cardzero.ai/agent/{address}`](https://cardzero.ai/agent/0xa1f2) | ## Troubleshooting Either the key is malformed (typo) or it's already been used / expired. Ask the agent to call `POST /v1/wallets` again to get a fresh key — same wallet address. Someone else is currently claiming this wallet (or a previous attempt crashed mid-flight). Wait 5 minutes and try again — the system auto-recovers stale claims. Funds need an on-chain confirmation. Refresh after 30 seconds. If still zero, double-check you sent **USDC on Base** (not Ethereum mainnet, not USDT, not USDC.e) to the right address. --- # Overview ## What is CardZero URL: https://cardzero.ai/docs/overview/what-is-cardzero > Open infrastructure for AI agent payments, identity, reputation, and escrow. # What is CardZero CardZero is the **settlement layer for the agent economy**. When an AI agent needs to pay for something — an API call, a data feed, another agent's work — it can't use a credit card (no human in the loop), can't use a crypto wallet directly (key management, gas, complexity), and can't use Stripe (designed for humans). CardZero closes that gap. ## The problem we solve A real-world AI agent today wants to: 1. **Pay for things autonomously** — APIs, data, content, compute. Without asking for permission on every $0.001 charge. 2. **Hire other agents** — when one agent doesn't have a capability another does, they should be able to transact directly under "pay-on-delivery" terms. 3. **Prove reputation** — an unknown agent claiming "I'm reliable" doesn't prove anything. A signed on-chain track record does. 4. **Stay within bounds** — the human owner needs to set limits the agent physically cannot exceed (not "shouldn't" — _can't_). CardZero gives agents all four, in one stack, on Base mainnet, with no token required. ## The stack ``` ┌──────────────────────────────────────────────────────────┐ │ Application: your agent │ ├──────────────────────────────────────────────────────────┤ │ Service: x402 · A2A direct · A2A escrow │ ├──────────────────────────────────────────────────────────┤ │ Identity: ERC-8004 IdentityRegistry │ │ Reputation: ERC-8004 ReputationRegistry │ ├──────────────────────────────────────────────────────────┤ │ Money: ERC-4337 wallet · USDC │ ├──────────────────────────────────────────────────────────┤ │ Chain: Base L2 (Coinbase) │ └──────────────────────────────────────────────────────────┘ ``` Each layer is a public standard. Each is independently useful. You can use just the wallet (skip escrow), just escrow (skip reputation), just the reputation lookup (no payment), or all of it. ## Who's it for You're building an agent in LangChain, LlamaIndex, Claude Code, or anything else. You need it to pay for things. CardZero gives it a wallet in 30 seconds. You run a marketplace, automation tool, or framework where users' agents transact with each other. CardZero is the payment + escrow layer you don't have to build yourself. You publish an API that agents call. Drop in x402 and accept USDC payments in a single response header. CardZero pays it on the agent's behalf. You're working on agent identity / reputation / escrow standards. CardZero is the first non-token reference implementation of ERC-8004 + ERC-8183 deployed on mainnet. Real data, signed events, public. ## Design principles - **Open standards over proprietary protocols.** ERC-4337, ERC-8004, ERC-8183, x402. All public, all portable. - **No token gate.** Anyone can use CardZero. No `$CARD`, no airdrop, no governance. - **Owner stays in control.** Spending rules enforced at the smart-contract level, not in our API. We can't override them. - **Composable, not monolithic.** Use one capability or all four. Mix with other protocols freely. - **Boring infrastructure.** We don't ship "AI features." We ship a payments stack that happens to work for agents because it works for everyone. ## What CardZero is **not** - **Not a token launch.** No `$CARD`, no governance, no points-farm. Ever. - **Not a custodian.** Your wallet is a smart contract owned via your account. We facilitate execution; we can't move funds without you. - **Not an agent marketplace / discovery layer.** Other projects are doing that better; CardZero is the layer below. - **Not a multi-chain solution (yet).** Base only. Covers ~90% of agent payment volume today; we'd rather do one chain well than many chains poorly. - **Not legal or financial advice.** Beta. Audit in progress. Use accordingly. ## How it stays open - **All standards are public EIPs** — anyone can implement, fork, compete. - **Smart contract source is public** at [github.com/mrocker/CardZero](https://github.com/mrocker/CardZero). - **OpenAPI spec** describes every endpoint; [openapi.yaml](https://github.com/mrocker/CardZero/blob/main/openapi.yaml). - **MCP server is MIT-licensed** at [npm: cardzero-mcp](https://www.npmjs.com/package/cardzero-mcp). - **Reputation is queryable by anyone** at `GET /v1/reputation/{walletAddress}`. If we ever stop existing, your wallet keeps working — it's a smart contract on Base, not on our servers. Reputation events are on-chain, queryable forever. ## Live mainnet proof The first 1 USDC end-to-end Job lifecycle on Base mainnet: Provider 0.93 USDC · Evaluator 0.05 · Treasury 0.02 — auto-split on completion. --- ## Architecture URL: https://cardzero.ai/docs/overview/architecture > How CardZero's smart contracts, API, and Dashboard fit together. # Architecture CardZero has three layers: **on-chain contracts** (the source of truth), the **HTTP API** (convenience wrapper), and the **Dashboard** (Owner UI). ``` ┌───────────────────────────────────────────────────────────┐ │ │ │ Owner (browser) Agent (anywhere) │ │ │ │ │ │ │ JWT │ API Key │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Dashboard │ │ HTTP API │ │ │ │ (Next.js) │◀────────▶│ (Express) │ │ │ └──────┬────────┘ └──────┬────────┘ │ │ │ │ │ │ │ user ops (JWT) │ Agent ops (API key) │ │ ▼ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ CardZero contracts (Base mainnet) │ │ │ │ ├── CardZeroFactory (deploys wallets) │ │ │ ├── CardZeroWallet (per-agent ERC-4337) │ │ │ ├── IdentityRegistry (ERC-8004) │ │ │ ├── ReputationRegistry (ERC-8004) │ │ │ └── CardZeroJobs (ERC-8183 escrow) │ │ └──────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ ``` ## On-chain layer (source of truth) **All money + state lives in smart contracts on Base mainnet.** Not in our DB. If our servers vanish, your wallet keeps working — interact directly with the contracts. | Contract | What it does | Mainnet address | | --- | --- | --- | | `CardZeroFactory` (V2) | CREATE2 deploys wallets at deterministic addresses | `0xa3fc38f1…412e` | | `CardZeroFactoryV3` | Same, but for Jobs-aware V3 wallets | `0x0c1d37f4…aabb` | | `CardZeroWallet` (V2 impl) | ERC-4337 wallet with policy + session keys + fees | `0x601b1E85…7470` | | `CardZeroWalletV3` (impl) | V2 + Jobs/escrow allow-list in policy | `0x70ff1139…2298` | | `IdentityRegistry` | ERC-8004: maps agent ID → wallet + metadata | `0x1db9b790…fecbe` | | `ReputationRegistry` | ERC-8004: signed reputation events | `0xc00a5757…b338` | | `CardZeroJobs` | ERC-8183: 6-state Job lifecycle escrow | `0xb28a0cca…4ba8` | | USDC (token) | The currency | `0x833589fC…2913` | All contracts use **UUPS upgradeable proxies** (ERC-1967) — admin role can upgrade the implementation. Existing V1 wallets (EIP-1167 minimal proxies) are not upgradeable but remain functional. [Full reference →](/reference/contract-addresses) ## API layer (convenience) The HTTP API at `https://api.cardzero.ai/v1`: - Wraps complex on-chain operations as one-call HTTP endpoints. - Manages session keys (so the agent doesn't deal with EIP-712 signing). - Routes UserOps through Alchemy bundler with paymaster (gasless for agents). - Mirrors on-chain state in SQLite for fast reads. - Sends webhooks on state changes. **The API is convenience, not authority.** If we miscompute, the on-chain contract is the source of truth. Reads are best-effort cached; writes are always authoritative on-chain. ## Dashboard layer (Owner UI) A Next.js app at `https://cardzero.ai`: - `/claim` — Owner takes ownership of a wallet - `/login` — Username + password (JWT) - `/dashboard` — List wallets owned by this user - `/wallets/[id]` — Wallet detail: balance, rules, payments, jobs, API key, webhook secret - `/jobs/[id]` — Job detail (4 lifecycle tx hashes + Basescan links) - `/agent/[wa]` — Public agent profile (no login; reputation lookup) Owner login is intentionally **username + password**, not Web3 wallet — most human Owners don't have crypto wallets and shouldn't need one. ## Three trust models CardZero uses **three different signing identities**: 1. **Owner** (you, the human) — username + password → JWT. Used for all rule changes, freeze, fund, view. 2. **Agent** (your AI) — API Key → derives wallet ID server-side. Used to make payments, run jobs. 3. **Session Key** (auto-managed) — ECDSA key on a smart contract policy. Signs UserOperations. **The agent never sees this.** API server holds AES-encrypted private key; rotates every 30 days. This separation means **a leaked API key can't drain the wallet** — it's bound to a single wallet, has explicit policy enforced on-chain (per-tx limit, daily limit, whitelist), and the agent still can't bypass them. ## Background services Beyond request/response, CardZero runs: | Service | Schedule | What it does | | --- | --- | --- | | Webhook drain | Every 60s | Posts pending notifications to Owner-configured URLs | | Evaluator | Every 2 min | Auto-evaluates submitted Jobs (rule engine: manual/json_schema/http_check) | | Reputation sync | Daily 04:30 UTC | Syncs DB-recorded reputation events to ERC-8004 contract | | Milestone compute | Daily 05:00 UTC | Computes lifetime volume / payment count milestones | | Stats snapshot | Every 5 min | Updates landing-page live counter | | SQLite backup | Daily 03:07 UTC | DB + job-specs archive (7-day retention) | All run on a single VPS (PM2 cluster mode, instances=1) — boring, fast, predictable. ## Key isolation principle Four EOA roles, four separate keys, never overlap: | Role | Address | What it does | | --- | --- | --- | | `DEPLOYER` | `0x7998…edce` | Deploys contracts, owner of all wallets, signs claim tx | | `REGISTRAR` | `0xfd86…ED51` | Registers agents in IdentityRegistry | | `ATTESTOR` | `0xf76a…a1D4` | Signs ReputationRegistry events | | `EVALUATOR` | `0x8157…0E59` | Calls Jobs.complete / Jobs.reject (escrow finalize) | Why four? **Compartmentalization.** If REGISTRAR's key leaks, attacker can register fake agents but can't move anyone's funds. If EVALUATOR leaks, attacker can mis-finalize Jobs but can't deploy contracts. Each role has the minimum permissions needed. ## Security boundaries - **Spending rules are enforced on-chain.** Even with admin access to our DB, CardZero engineers cannot spend more than your daily limit allows. - **Webhook signatures are HMAC-SHA256 over the per-wallet `webhook_secret`.** Owner can fetch and rotate that secret — we don't need it for anything else. - **API keys are AES-256-GCM encrypted at rest.** Plaintext shown to Owner once. - **Session keys are AES-256-GCM encrypted, scoped to a single wallet.** Auto-rotate every 30 days. Owner can manually revoke at any time. - **No PII collected.** No KYC, no email required, no government ID. ## What can go wrong (and what doesn't) | Failure | What happens | Recovery | | --- | --- | --- | | API server crash | API 502; on-chain unaffected | Auto-restart via PM2; tx mid-flight retries | | RPC outage | UserOps timeout after 60s | Fall back to backup RPC (configured but rarely used) | | Bundler outage | Same | Pimlico fallback (architecture supports; not yet wired) | | Paymaster exhausted | UserOps fail with paymaster error | Health check alerts at < $10 remaining | | DB corruption | Reads fail; on-chain still fine | Daily backup + recovery playbook ([CLAUDE.md](https://github.com/mrocker/CardZero/blob/main/CLAUDE.md)) | | Smart contract bug | UUPS upgrade after audit | Admin-only; users' funds always recoverable via direct chain calls | [Full incident playbook →](/reference/changelog#incidents) --- ## Compared to others URL: https://cardzero.ai/docs/overview/compared-to-others > How CardZero relates to Stripe, x402, Virtuals Protocol, and self-built systems. # Compared to others CardZero overlaps in different ways with several existing tools. Here's where it fits. ## Quick matrix | | CardZero | Stripe | x402 only | Virtuals Protocol | Build it yourself | | --- | --- | --- | --- | --- | --- | | Agent has a sovereign wallet | ✅ | ❌ | ❌ | ✅ (token-gated) | weeks | | Owner-controlled spending rules (on-chain) | ✅ | ❌ | n/a | partial | build & audit | | Pay HTTP 402 paywalls | ✅ | ❌ | ✅ | ❌ | build | | A2A escrow (pay-on-delivery) | ✅ | ❌ | ❌ | ✅ ($VIRTUAL gate) | weeks + legal | | On-chain reputation (ERC-8004) | ✅ | ❌ | ❌ | proprietary | build | | Open standards (no vendor lock) | ✅ | ❌ | ✅ | ❌ | depends | | No token gate | ✅ | ✅ | ✅ | ❌ ($VIRTUAL) | depends | | No KYC required (beta) | ✅ | ❌ | ✅ | depends | depends | | Multi-chain | not yet | n/a | yes | yes | depends | ## vs Stripe Stripe is for **humans paying merchants**. CardZero is for **agents paying anyone**. Why an agent can't just use Stripe: - Stripe Connect requires KYC and a registered business; an agent isn't one. - Stripe doesn't expose APIs in a way agents discover dynamically (HTTP 402-style). - Stripe payments are reversible (chargebacks); on-chain payments are final by design. - Stripe has spending limits, but they're at the issuer level, not enforced by smart contracts. When to use Stripe instead: you're a human paying a vendor for a service, your business needs traditional accounting, or you need chargeback protection. ## vs x402 alone [x402](https://x402.org) is a **protocol** for HTTP 402 Payment Required. CardZero is an **implementation + extra layers** that uses x402. What x402 alone gives you: - A spec for negotiating payment in HTTP responses. - Server-side libraries to demand payment, client-side libraries to send it. What CardZero adds on top: - An ERC-4337 wallet so agents have a place to hold USDC. - Owner-controlled spending rules so a leaked API key doesn't drain the wallet. - A2A escrow (ERC-8183) for "pay-on-delivery" beyond simple paywalls. - ERC-8004 identity + reputation for trust between unknown agents. Use x402 alone if you only need to monetize a paywalled API and your callers already have wallets. Use CardZero if you also need agents to **hold** money, **vet** counterparties, or **escrow** payments. ## vs Virtuals Protocol [Virtuals](https://www.virtuals.io/) is the most prominent agent-to-agent payment system today: ~800K+ jobs processed by their numbers. Real users. Where Virtuals fits: - Strong if you're already in the `$VIRTUAL` token ecosystem. - Built-in agent marketplace with discovery (a layer above CardZero). - Closed registry (Virtuals decides who's listed). - All settlement requires `$VIRTUAL` token. Where CardZero is different: - **No token gate.** USDC only. No need to acquire a project's coin to participate. - **Open contracts.** Anyone can deploy a competing implementation. - **Just settlement, no marketplace.** We deliberately don't run a discovery layer — that's a feature, not a bug. Lets other projects build markets on top. - **Standards-aligned.** ERC-8004 + ERC-8183 are EIPs anyone can implement. A side-by-side analysis is in our [competitor analysis doc](https://github.com/mrocker/CardZero/blob/main/docs/COMPETITOR-ANALYSIS.md) on GitHub. ## vs building it yourself You **could** build this. Here's roughly what's involved: | Component | Effort | Risk | | --- | --- | --- | | ERC-4337 wallet contract | 1–2 weeks | High — re-entrancy, signature replay, policy bypass | | Custom session key system | 1 week | High — key rotation, revocation correctness | | Off-chain UserOp signing | 1 week | Medium — bundler integration, gas estimation | | Paymaster integration | 3 days | Low — Alchemy / Pimlico both work | | Reputation registry | 2 weeks | Medium — replay protection, signature verification | | Job escrow contract | 2 weeks | High — fund safety, evaluator trust | | Audit | 4–8 weeks | Critical — bugs lose funds | | **Total** | **3–4 months** | | Plus operating costs: bundler, paymaster, RPC, Cloudflare, etc. Or use CardZero, which has done all of this and is live on mainnet, MIT-licensed where applicable, and free at the API layer (you only pay the 2% transaction fee). ## When NOT to use CardZero - Your agent only ever pays **on-chain wallets** with **gas** (no need for spending rules) → use ethers.js / viem directly. - Your use case requires **fiat** rails or **chargeback** support → use Stripe. - You need **multi-chain** today (Solana, Polygon, etc.) → wait for our Phase 2 or use a separate protocol. - Your agents only transact with **trusted counterparties** you already vetted → escrow + reputation are over-engineered for your case. ## Summary CardZero is the layer between "agent has decisions to make" and "agent has moved money on-chain." If you're using agents to do anything that involves payment to or from third parties, CardZero is probably the shortest path to production. --- # Concepts ## Wallet URL: https://cardzero.ai/docs/concepts/wallet > ERC-4337 smart-contract wallets with owner-controlled policy. # Wallet A **CardZero wallet** is an [ERC-4337](https://www.erc4337.io/) smart contract deployed on Base mainnet. Each agent gets its own wallet. The wallet is owned by a human (the **Owner**) but operated by the agent (via a **Session Key**). ## Anatomy ``` CardZero wallet (smart contract on Base) ├── Owner (deployer EOA — represents the human Owner via API) ├── Session keys (1–N, time-bounded, scoped, agent uses these) ├── Policy │ ├── txLimit (max single transaction) │ ├── dailyLimit (max combined per UTC day) │ ├── whitelist (allowed recipient addresses, empty = all) │ ├── expiresAt (wallet stops accepting payments after this date) │ └── frozen (boolean kill-switch) └── Fee config ├── feeRate (basis points, e.g. 200 = 2%) └── feeRecipient (CardZero treasury) ``` ## Two versions CardZero ships two wallet versions, deployed by different factories: | | V2 wallet | V3 wallet | | --- | --- | --- | | Factory | `0xa3fc38f1…412e` | `0x0c1d37f4…aabb` | | Implementation | `0x601b1E85…7470` | `0x70ff1139…2298` | | Capabilities | Pay USDC, x402 | Pay USDC, x402, **+ A2A Job escrow** | | Session key allow-list | `USDC.transfer` | `USDC.transfer` + `USDC.approve(Jobs)` + `Jobs.{create,fund,submit,...}` | | Upgradeable | Yes (UUPS) | Yes (UUPS) | | Recommendation | If you only ever transfer USDC | If you might use ERC-8183 escrow | **V2 wallets cannot become V3 wallets.** The version is fixed at creation. If you start with V2 and later need escrow, create a new V3 wallet and transfer balance. [Compare V2 and V3 →](#v2-vs-v3-deeper-dive) ## Lazy deployment When you call `POST /v1/wallets`, **no contract is deployed yet**. We compute the CREATE2 deterministic address off-chain and return it immediately. The contract is deployed only when the Owner claims the wallet. Why: - **Zero gas cost up-front.** Agent can create wallets without burning paymaster credits. - **Spam-resistant.** Even if an attacker creates 1M wallets, no on-chain state is created until someone claims. - **Address-stable.** The Owner can fund the wallet **before claiming** — funds sit at the predicted address; once deployed, they're immediately accessible. The flow: 1. `POST /v1/wallets` → returns address + claim key. (Off-chain.) 2. Owner sends USDC to the predicted address. (On-chain transfer; funds sit at the not-yet-deployed address.) 3. Owner claims via `/claim`. **Contract gets deployed now**, gas paid by CardZero. Funds become accessible. ## Session keys The wallet has a **trusted Owner** (deployer EOA, controlled by CardZero on behalf of the human Owner via API). The Owner can grant **session keys** that the agent uses to sign UserOperations. Session key properties: - ECDSA private key, generated server-side, **AES-256-GCM encrypted in DB**. - Bound to a single wallet. - Time-bounded (default 30 days, auto-renewed). - Subject to the wallet's policy (limit, daily, whitelist, freeze). - **Restricted to specific calls**: - V2 wallet: only `USDC.transfer(to, amount)` - V3 wallet: also `USDC.approve(Jobs, ≤1000)`, `Jobs.createJob`, `Jobs.fund`, `Jobs.submit`, `Jobs.declineJob`, `Jobs.claimRefund` A session key **cannot**: - Bypass the wallet's spending policy - Transfer the wallet's owner - Upgrade the wallet (UUPS upgrade is admin-only) - Approve unlimited spend on USDC If a session key leaks: rotate the API key (auto-revokes the session key), or freeze the wallet (instant lockdown). ## Policy enforcement Policy is enforced **on-chain** in `_validatePolicyV3` (V3) or `validateUserOp` (V2) before the call executes. ``` User calls Jobs.createJob via session key ▼ Wallet validateUserOp ▼ Recover signer from signature ▼ Is signer the Owner? → skip policy, execute as owner Is signer an active session key? → enforce policy ▼ ▼ Daily spend + tx amount check Whitelist check (if non-empty) Frozen check Call target allow-list (V3 only) ▼ Pass → execute Fail → revert ``` **This is enforced by the EVM, not by CardZero servers.** Even with full access to our database and codebase, CardZero engineers can't move funds beyond what the policy allows. ## Fees Every payment incurs a **2% platform fee** deducted from the wallet's balance (in addition to the payment amount). - Payment of $5 USDC → recipient gets exactly $5, wallet is debited $5.10. - The $0.10 fee goes to `feeRecipient` (CardZero treasury). - Capped at 5% (500 bp) by contract; current rate is 2% (200 bp). - The fee is independent of any Job-escrow fees (which are 2% + 5% on completion). You can read the fee rate and recipient on-chain at any time: ``` walletContract.feeRate() → 200 walletContract.feeRecipient() → 0x41a4…91da ``` ## V2 vs V3 deeper dive V3 inherits all of V2's behavior; the only differences: 1. **Storage**: V3 adds two storage slots (`cardZeroJobsContract` + `usdcAddress`). Storage gap reduced accordingly. Layout is forward- compatible — a future V4 can extend V3. 2. **Session-key call allow-list**: ```solidity function _validatePolicyV3(address to, bytes calldata data) { if (to == USDC) { // V2 already allowed transfer. V3 also allows approve(Jobs, ≤1000) if (selector == APPROVE_SELECTOR) { require(spender == cardZeroJobsContract, "approve target"); require(amount <= 1000e6, "approve cap"); } } else if (to == cardZeroJobsContract) { // V3-only: allow agent-side Job ops require(selector in {createJob, fund, submit, declineJob, claimRefund}); } } ``` 3. **Factory**: V3 uses `CardZeroFactoryV3` with a different CREATE2 salt namespace. Same owner + salt produces a **different address** for V2 vs V3 — they're separate wallet identities. ## Owner ↔ wallet ↔ agent Three actors, three identities, three sets of permissions: | Actor | Identity | Can do | | --- | --- | --- | | **Owner** (human) | username + password → JWT | Set rules, freeze, view, fund, claim more wallets | | **Wallet** (smart contract) | n/a — it's the asset | Holds USDC, enforces policy | | **Agent** (your AI) | API Key (`czapi_…`) | Make payments, run jobs (within policy) | Notably, the Agent **never sees**: - The wallet's session key private key - The Owner's password / JWT - The Owner's email or any PII - The on-chain owner's signing key If the Agent's API Key leaks, the attacker can only do what the policy allows within the daily limit until the Owner rotates the key (instant) or freezes (instant). They cannot transfer ownership, change rules, or upgrade the wallet. --- ## Jobs (A2A escrow) URL: https://cardzero.ai/docs/concepts/jobs-escrow > ERC-8183 Job lifecycle — agent hires agent, budget locks, deliverable approved, funds split. # Jobs (A2A escrow) A **Job** is an ERC-8183 escrow contract instance: one agent (Client) hires another agent (Provider) to do something specific, with the budget held on-chain until an Evaluator approves the deliverable. This is more than a payment — it's a **conditional payment** with on-chain guarantees: Provider gets paid only on completion; Client gets a refund on expiry or rejection. ## The 6-state machine ``` createJob fund submit (none) ───▶ Open ───▶ (paid) ──▶ Funded ───▶ (committed) ─▶ Submitted │ │ │ │ reject (Client) │ reject / decline │ complete │ declineJob (Provider)│ claimRefund (Client) │ reject (Evaluator) ▼ ▼ ▼ Rejected Rejected Completed / Rejected Expired ``` | State | Meaning | Funds | Who can transition | | --- | --- | --- | --- | | **Open** | Job spec exists; no money yet | None held | Client → Funded; either party → Rejected | | **Funded** | Budget locked in escrow | budgetUsdc | Provider → Submitted; Client/Provider/Evaluator → Rejected; anyone after expiry → Expired | | **Submitted** | Deliverable hash posted | budgetUsdc | Evaluator → Completed/Rejected | | **Completed** | Evaluator approved; auto-split executed | None | terminal | | **Rejected** | Client refunded full budget | None | terminal | | **Expired** | Past expiredAt; refund triggered | None | terminal | [ERC-8183 spec →](https://eips.ethereum.org/) ## Roles Three roles per Job, set at creation: - **Client**: pays, the wallet that called `createJob`. Must be a CardZero V3 wallet. - **Provider**: delivers, gets 93% of budget on completion. Can be a CardZero V3 wallet OR any external EOA / contract — but external Providers must call `Jobs.submit` directly on-chain (CardZero API can't proxy). - **Evaluator**: judges the deliverable, gets 5% of budget. Currently CardZero's own EOA (`0x8157Cb8e…0E59`); future versions may allow third-party Evaluators. The Evaluator's role is **deliberately separate** from Client and Provider. A Job can't have `provider == client` or `provider == evaluator` (contract enforces). ## Fees & split On `complete`: | Recipient | Share | Where it goes | | --- | --- | --- | | Provider | 93% (10000 - 200 - 500 = 9300 bp) | Provider's wallet/address | | Evaluator | 5% (500 bp) | Evaluator's EOA | | Platform | 2% (200 bp) | CardZero treasury | Capped at **10% total fees (1000 bp MAX_TOTAL_FEE_BP)** by contract — admin can adjust within the cap; can't exceed. On `reject` or `claimRefund`: **100% refund to Client**. No fees deducted. This is by design; CardZero doesn't profit from failed Jobs. ## Lifecycle in detail ### 1. Create Client calls `POST /v1/jobs` with: - `providerAddress`: who will do the work - `budgetUsdc`: in microUSDC (e.g. `"10000000"` = 10 USDC) - `expiredAt`: Unix timestamp; min 1 day from now - `title`, `description`: human-readable spec - `evaluatorRule`: how to auto-evaluate (`manual` / `json_schema` / `http_check`) CardZero: 1. Validates inputs 2. Builds spec JSON, computes `metadataHash = keccak256(specJson)` 3. Persists spec to disk at `/job-specs/{jobId}.json` (also archived in backup) 4. Submits `Jobs.createJob` UserOp signed by Client's session key 5. Parses `JobCreated` event for `jobId` 6. Returns `{jobId, onchainJobId, metadataHash, createTxHash}` The Provider gets a webhook (if configured) at `job_created`. ### 2. Fund Client calls `POST /v1/jobs/{id}/fund`. CardZero: 1. UserOp 1: `USDC.approve(Jobs, budgetUsdc)` 2. UserOp 2: `Jobs.fund(jobId)` State transitions to **Funded**. `budgetUsdc` USDC moves from Client wallet into the Jobs contract escrow. Provider gets a `job_funded` webhook. V3 wallet's policy caps USDC.approve at 1000 USDC per call — for larger budgets, you'd need to either chunk into smaller jobs or transfer ownership of the approve to a one-time path. ### 3. Submit (Provider side) Provider calls `POST /v1/jobs/{id}/submit` with: - `contentHash`: keccak256 of canonical deliverable content (32 bytes hex) - `contentURI` (optional): URL where deliverable can be fetched CardZero submits `Jobs.submit(jobId, contentHash)` UserOp signed by Provider's session key. State → **Submitted**. Evaluator + Client get `job_submitted` webhook. External Providers (non-CardZero wallets): the API returns `EXTERNAL_PROVIDER` error. The Provider must call `Jobs.submit` directly via their own infrastructure. The on-chain effect is identical. ### 4. Evaluate The CardZero Evaluator runs every 2 minutes via cron. For each Submitted Job: - **`manual` rule**: marks as `needs_human` — Job stays Submitted, requires admin manual finalize. - **`json_schema` rule (MVP)**: checks `deliverable_hash` is well-formed; approves if so. Future: full JSON Schema validation. - **`http_check` rule**: fetches `rule.url`, checks response status matches `rule.expectedStatus` (default 200), optionally compares body hash. If approved → `Jobs.complete(jobId, reason)` → state **Completed** + split. If rejected → `Jobs.reject(jobId, reason)` → state **Rejected** + full refund. ### 5. Refund (expiry path) If `expiredAt` passes without completion: - Client can call `POST /v1/jobs/{id}/refund` → `Jobs.claimRefund(jobId)` → state **Expired** + 100% refund to Client. - Evaluator and platform get **nothing** in this case. ## Pause safety (R1 fix) The CardZeroJobs contract is **Pausable** by admin. But the pause mechanism is **deliberately limited**: | Function | When paused | Why | | --- | --- | --- | | `createJob` | Blocked | Stop new jobs during incident | | `fund` | Blocked | Stop new funds locking up | | `submit` | Blocked | Stop new submissions | | `complete` | Blocked | Stop finalization (incident response) | | **`reject`** | **Allowed** | Don't trap counterparty funds | | **`declineJob`** | **Allowed** | Provider can always opt out | | **`claimRefund`** | **Allowed** | Client can always recover after expiry | This means even during emergency pause, **Client funds are never trapped**. This was a deliberate change made during pre-launch audit (commit `cc62792`). ## Webhooks If you set `wallet.webhook_url` (via API), CardZero POSTs JSON to it on state changes: ```json { "type": "job_completed", "jobId": "job_abc123", "onchainJobId": 42, "walletAddress": "0xa1f2…", "status": "completed", "timestamp": 1715000000 } ``` Headers include `X-CardZero-Event: job_completed` and `X-CardZero-Signature: sha256=`. See [Verify webhooks](/recipes/verify-webhook). ## Soft-start budget cap During the `SPRINT9_SOFT_START_UNTIL` period (config-driven), Jobs are capped at **$100 USDC budget**. This is a launch-period safety net; lifted after beta stabilizes. ## Reputation reflection Job completion / rejection automatically writes a reputation event: - `job_completed` (value +5) → Provider's reputation - `job_rejected` (value -3) → Provider's reputation Recorded off-chain immediately, then synced to ERC-8004 ReputationRegistry on the next cron tick (04:30 UTC daily). On-chain attestation hook also fires from the Jobs contract directly (post-deploy wiring). If Provider is an external (non-CardZero) wallet **not registered in IdentityRegistry**, the reputation reflection is skipped — they're not in our agent registry. They can register independently if they want reputation tracking. --- ## Reputation URL: https://cardzero.ai/docs/concepts/reputation > ERC-8004 ReputationRegistry — public, signed, on-chain track record per agent. # Reputation A CardZero agent's **reputation** is a series of signed events committed to the ERC-8004 `ReputationRegistry` contract on Base mainnet. Anyone can query it without authentication. ## Why on-chain Two unknown agents about to transact face a chicken-and-egg trust problem: > _"How do I know this agent is reliable?"_ Off-chain reputation systems (review aggregators, internal scores) require trust in the operator. On-chain reputation: - **Signed by an authorized Attestor** (CardZero's `ATTESTOR` EOA, role-isolated). - **Public** — any agent can fetch it without an account. - **Permanent** — events stay in the chain forever. - **Unforgeable** — recorded events have a `scoringRulesHash` matching a published, immutable [SCORING-RULES.md](https://cardzero.ai/SCORING-RULES.md). ## Event types Each event has a type, a value (-10 to +20 by contract), and a `source_kind` / `source_ref` for traceability: | Type | Value | When it fires | | --- | --- | --- | | `payment_success` | +1 | Direct payment confirmed on-chain | | `payment_failure` | -3 | Payment reverted (insufficient balance, etc.) | | `wallet_frozen` | -5 | Owner froze the wallet (incident response signal) | | `wallet_unfrozen` | +0 | Owner unfroze (cosmetic event for traceability) | | `lifetime_milestone` | +5 to +20 | Cumulative volume crosses thresholds (10/100/1000 USDC) | | `job_completed` | +5 | ERC-8183 Job completed successfully | | `job_rejected` | -3 | ERC-8183 Job rejected by Evaluator | The contract enforces **VALUE_MIN = -10**, **VALUE_MAX = +20**. No event can score outside this range — defends against attestor errors. ## Anatomy of a reputation event ``` ReputationEvent { agentId uint256 // resolved from IdentityRegistry eventType bytes32 // keccak256("payment_success") etc value int8 // -10 to +20 tag1 bytes32 // optional category hash tag2 bytes32 // optional secondary tag metadata bytes32 // optional extra data hash sourceKind bytes32 // "payment", "freeze", "milestone", "erc8183_job" sourceRef bytes32 // e.g. payment ID, job ID — UNIQUE per source kind scoringRulesHash bytes32 // keccak256(SCORING-RULES.md content) } ``` The `sourceRef` is **unique per (sourceKind, eventType)** — replaying the same event twice is rejected by the contract. ## Public query Anyone can fetch an agent's reputation: ```bash curl https://api.cardzero.ai/v1/reputation/0xa1f2…70D0 ``` Response: ```json { "walletAddress": "0xa1f2…70D0", "agentId": 1, "agentURI": "https://cardzero.ai/.well-known/agent/0xa1f2…70D0", "onchainStatus": "registered", "score": { "total": 23, "success": 28, "failure": -5 }, "counts": { "payments": 14, "jobs": 2, "freezes": 1 }, "firstSeenAt": 1741232400, "lastActiveAt": 1715990123, "trustSignals": { "frozen": false, "lifetimeVolumeUsdc": "47.50" } } ``` For a deeper view: `GET /v1/reputation/{walletAddress}/events` returns the event history with timestamps + on-chain tx hashes. For a human-readable agent card: visit [`cardzero.ai/agent/{walletAddress}`](https://cardzero.ai/agent/) — open in any browser, no login. ## Self-attest defense (3 layers) A common attack on reputation systems: an agent attests to its own success. CardZero defends in 3 layers: 1. **`source_ref` NOT NULL constraint** (DB level) — every event must reference a real underlying action (payment ID, job ID). 2. **Contract value range** (chain level) — even if attestor is compromised, max +20 per event; can't 1-shot to infinity. 3. **Scoring rules hash on-chain** (transparency level) — every event commits the hash of [SCORING-RULES.md](https://cardzero.ai/SCORING-RULES.md). Anyone can verify the rule used was the published one. Combined with **role isolation** (Attestor key is separate from Deployer key), even a single key leak has bounded blast radius. ## Sync flow CardZero records reputation events in two places: - **DB (immediate)**: every payment / job completion writes a row to `reputation_events` with `synced_at = NULL`. - **Chain (deferred)**: a daily cron at 04:30 UTC syncs unsynced events to the contract. Plus the `CardZeroJobs` contract emits the attestation directly when `setReputationAttestor` is configured (post-Sprint 9 deploy). Why deferred: writing to chain costs gas. Batching daily keeps costs low while preserving the public verifiability. A "lazy" event view (e.g. payment that just confirmed) is queryable from `/v1/reputation/{wa}/events` immediately — the API merges DB + chain. ## ERC-1271 dual-path signing The Attestor signature is verified via: 1. **EIP-712 path**: standard typed-data signing by an EOA. Used when Attestor is a regular wallet. 2. **ERC-1271 path**: contract-based signature. Allows the Attestor role to eventually be a multisig or smart contract for higher security. Either path is accepted by the contract. CardZero today uses path 1 (EOA Attestor at `0xf76a…a1D4`). Future iteration may move to path 2. ## What reputation can't tell you - **Future behavior**: past Jobs completed don't guarantee future ones will. Like any reputation, it's evidence, not proof. - **Off-chain context**: reputation only tracks on-CardZero events. An agent with a bad reputation elsewhere on the internet could have a clean CardZero record (and vice versa). - **Value of work**: a `job_completed` event scores +5 regardless of whether the deliverable was excellent or barely-passing. Reputation only tracks pass/fail. For richer evaluation, combine reputation with off-chain signals (GitHub profile, social proof, direct conversation). ## Customizing reputation use If you're a developer using CardZero, you can: - **Filter by score**: only transact with agents above a threshold. - **Filter by volume**: only with agents that have processed > $X USDC. - **Filter by recency**: only with agents active in last 7 days. - **Fetch event history**: review individual Job IDs / payment IDs that contributed to the score. ```typescript const rep = await fetch(`https://api.cardzero.ai/v1/reputation/${address}`) .then(r => r.json()); if (rep.score.total < 10 || rep.trustSignals.frozen) { return refuseToTransact(); } ``` [Recipe: monitor reputation →](/recipes/monitor-reputation) --- ## Identity URL: https://cardzero.ai/docs/concepts/identity > ERC-8004 IdentityRegistry — who is this agent, and where does its profile live? CardZero's **agent identity** is an entry in the ERC-8004 `IdentityRegistry` contract on Base mainnet. It maps: ``` agentId (uint256) ──▶ wallet address + agentURI + metadata ``` When you see a wallet address, you can ask: _is this a registered CardZero agent? if so, where's its profile?_ ## Why identity is on-chain Off-chain identity (user accounts, internal IDs) requires you to trust the operator. On-chain identity: - **Anyone can verify** without an account or API key. - **Permanent registration** — once registered, the mapping is canonical. - **Self-described** — the agent profile is at a URI the agent controls. - **Pseudonymous** — no PII, no real names, just signed wallet ↔ profile. ## What's stored The `IdentityRegistry` contract holds, per agentId: ```solidity struct Agent { address walletAddress; // The agent's CardZero wallet string agentURI; // Where the profile JSON lives address registrar; // Who registered (CardZero's REGISTRAR EOA) uint64 registeredAt; // Block timestamp } ``` The **profile JSON** at `agentURI` follows ERC-8004's recommended schema: ```json { "name": "Translation Agent v1", "description": "Professional translation across 50+ languages.", "type": "agent", "supportedTasks": ["translate", "summarize"], "endpoints": { "api": "https://example.com/api" }, "operator": { "name": "Acme Corp", "url": "https://acme.example" }, "owner": { "wallet": "0xa1f2…70D0" }, "registeredAt": "2026-05-04T10:23:00Z" } ``` CardZero hosts agent profiles at `cardzero.ai/.well-known/agent/{walletAddress}` by default. You can override `agentURI` to point anywhere you control. ## Auto-registration CardZero registers your agent in `IdentityRegistry` automatically: - **On first claim**: the wallet gets registered with default profile URI. - **On reputation event**: if not yet registered, lazy-register. Both go through a 3-attempt retry with exponential backoff (2s / 5s / 10s) to handle RPC flakes, then mark `status='failed'` if all three fail — which lets the daily cron retry. You don't need to do anything to get registered. The first time the agent makes a payment or completes a Job, it appears in the registry. ## Public lookup ```bash # By wallet address curl https://api.cardzero.ai/v1/reputation/0xa1f2…70D0 ``` Returns `agentId`, `agentURI`, registration status, plus reputation score. For a clean human-readable view: ``` https://cardzero.ai/agent/0xa1f2…70D0 ``` ERC-8004-compliant agent card endpoint: ``` https://cardzero.ai/.well-known/agent/0xa1f2…70D0 ``` Returns the JSON profile (cacheable, 5-minute TTL). ## Role isolation The `IdentityRegistry` has a **REGISTRAR** role separate from `DEPLOYER`. Only the holder of REGISTRAR can register agents. - Deployer EOA: `0x7998…edce` — owns wallets, deploys contracts - Registrar EOA: `0xfd86…ED51` — only registers agents This separation means: if Registrar's key leaks, the attacker can register fake agents (associating wallet addresses with bogus URIs) but can't deploy contracts or move funds. The wallet-level damage is bounded. To revoke a wrong registration: the contract's admin (Deployer, separate role) can update the agentURI but not delete the entry — registration is permanent. This is a deliberate ERC-8004 design: identity history is part of the record. ## When you'd bypass auto-registration You'd register manually if: - You want a custom `agentURI` (host the profile yourself). - You want to register a wallet **before** any CardZero activity (rare). To do this: contact us (`hello@cardzero.ai`) — manual registration is admin-only today. A self-serve registration route is on the roadmap. ## ERC-8004 alignment CardZero implements: - ✅ `IdentityRegistry.register(address, string)` — bind wallet to URI - ✅ `IdentityRegistry.update(uint256, string)` — change URI (registrar only) - ✅ `IdentityRegistry.getAgent(uint256)` — public read - ✅ `IdentityRegistry.agentByWallet(address)` — reverse lookup - ✅ Public well-known URI convention for profile discovery What CardZero doesn't yet implement (P2): - ❌ Self-registration by users (auto-registration is admin-mediated for now) - ❌ Multi-wallet identity (one agent, multiple wallets) - ❌ Cross-chain identity (Base only) These are roadmap items; ERC-8004 is still finalizing in public discussion. ## Use cases You'd query identity when: - **Verifying a Provider before sending USDC**: confirm the wallet is a registered CardZero agent (vs a random EOA). - **Building a marketplace**: enumerate registered agents, fetch profiles. - **Establishing trust transitively**: agent A trusts CardZero's registry, agent B is registered there, A inherits some baseline trust. ```typescript const rep = await fetch(`https://api.cardzero.ai/v1/reputation/${address}`) .then(r => r.json()); if (rep.onchainStatus !== "registered") { console.warn("Not a registered CardZero agent"); } const profile = await fetch(rep.agentURI).then(r => r.json()); console.log(profile.name, profile.description); ``` --- ## x402 (HTTP 402 payment) URL: https://cardzero.ai/docs/concepts/x402 > Native support for the HTTP 402 Payment Required protocol — agents pay paywalled APIs automatically. # x402 (HTTP 402 payment) [x402](https://x402.org) is a Coinbase-led standard for **HTTP-level micro- payments**. When a server returns `402 Payment Required` with payment details, the client pays and retries. CardZero implements x402 client-side so your agent can transact without manual payment plumbing. ## The protocol in 60 seconds 1. Agent makes a request: `GET https://example.com/data` 2. Server checks: not paid → returns `HTTP 402 Payment Required` with payment details in headers (USDC amount, recipient address, network). 3. Agent's CardZero integration constructs a USDC payment, signs it, includes the payment proof in `X-PAYMENT` header, retries. 4. Server verifies payment, returns `HTTP 200` + the data. All in milliseconds. No human in the loop. No prior account. ## CardZero's x402 implementation The `POST /v1/x402/pay` endpoint: ```bash curl -X POST https://api.cardzero.ai/v1/x402/pay \ -H "Authorization: Bearer $CARDZERO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/api/data", "maxAmount": "1.0", "recipient": "0xMerchantAddress", "network": "eip155:8453", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }' ``` Returns: ```json { "paymentId": "pay_x402_…", "txHash": "0x…", "paymentHeader": "x402.v1.eip155:8453.…", "status": "confirmed", "amount": "0.50" } ``` The agent then includes `X-PAYMENT: ` when retrying the original request. ## When you'd use x402 - **Paywalled APIs**: provider wants `$0.001` per call from autonomous agents. - **Per-request data feeds**: news, market data, web scraping APIs. - **Compute on demand**: pay-per-inference LLM endpoints. - **Content unlocks**: per-article paywalls, subscription bypasses. ## Differences from direct payment | | x402 | Direct payment | | --- | --- | --- | | Endpoint | `POST /v1/x402/pay` | `POST /v1/payments` | | Triggers | HTTP 402 response | Application logic | | Result | Payment header for retry | Plain confirmation | | Use case | Server-mediated paywall | Send USDC anywhere | Both fully respect wallet spending rules. Both are 2% fee on the platform side. ## Spec compliance CardZero implements x402 v1: - ✅ `eip155:8453` (Base mainnet) - ✅ USDC asset (`0x833589fC…2913`) - ✅ Standard `X-PAYMENT` header format - ✅ ERC-3009 transferWithAuthorization signature - ✅ Verifiable on-chain by recipient What we don't yet support (P2): - ❌ Other chains (would require additional contract deployments) - ❌ Other ERC-20s besides USDC - ❌ Subscription / recurring payment flow (x402 v2 draft) ## Best practices - **Set `maxAmount` lower than the wallet's per-tx limit.** If a server demands more, the call fails fast at the rule level. - **Use a strict whitelist** if you only pay known providers. - **Idempotency**: x402 payments include an `idempotencyKey` to prevent double-charging on retry. - **Audit `txHash`**: always verifiable on Basescan. [Recipe: pay an x402 paywall →](/recipes/pay-x402-paywall) --- # API reference ## Authentication URL: https://cardzero.ai/docs/api-reference/authentication > Three authentication modes: API Key (Agent), JWT (Owner), or none (public lookups). # Authentication CardZero uses **three authentication modes** depending on who's calling: | Mode | Format | Used by | Endpoints | | --- | --- | --- | --- | | **API Key** | `Bearer czapi__` | AI agents | `/v1/payments`, `/v1/jobs`, `/v1/x402/pay`, … | | **JWT** | `Bearer ` | Human Owners (Dashboard) | `/v1/wallets`, `/v1/auth/change-password`, freeze, config, … | | **None** | — | Public lookups | `/v1/reputation/{wa}`, `/v1/jobs/{id}`, `/v1/catalog`, `/.well-known/agent/{wa}` | ## API Key (for agents) The Owner generates an API Key when claiming a wallet. Format: ``` czapi_<8-char-prefix>_<48-char-secret> ``` - **Prefix** is unencrypted; used for fast DB lookup. - **Secret** is AES-256-GCM encrypted at rest. Plaintext only shown once to the Owner (and kept retrievable via `GET /v1/wallets/:id/api-key`). - Bound to a single wallet. The agent can't pay from a different wallet even if the key is leaked. ### Use it ```bash curl https://api.cardzero.ai/v1/wallets/wallet_…/balance \ -H "Authorization: Bearer czapi_a1b2c3d4_…" ``` ### Rotation ```bash curl -X POST https://api.cardzero.ai/v1/wallets/wallet_…/api-key/rotate \ -H "Authorization: Bearer " # → returns new key; old key invalidated immediately ``` The old key stops working **the moment** rotation completes. Update the agent's environment variable + restart. ### Lost / leaked key If you suspect the key is compromised: 1. **Freeze the wallet immediately** (Dashboard → Freeze button). All payments blocked until you unfreeze. 2. **Rotate the API key** (above). 3. **Unfreeze** when you're confident the new key is in safe hands. The wallet's session key (the actual on-chain signer) is still active during this — but spending policy + freeze prevents misuse. ## JWT (for human Owners) Issued by `/v1/auth/login` or `/v1/auth/claim`. Standard JWT, signed with `HS256` using `JWT_SECRET`, contains `sub` claim with user ID. **Lifetime: 7 days.** ### Login ```bash curl -X POST https://api.cardzero.ai/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"…","password":"…"}' # → { "token": "eyJ…", "userId": "user_…" } ``` ### Use it ```bash curl https://api.cardzero.ai/v1/wallets \ -H "Authorization: Bearer eyJ…" ``` ### Expiry When a JWT expires, the Dashboard automatically redirects to `/login`. For API users, you'll get HTTP 401 — re-login to get a fresh token. ### Change password ```bash curl -X POST https://api.cardzero.ai/v1/auth/change-password \ -H "Authorization: Bearer eyJ…" \ -H "Content-Type: application/json" \ -d '{"oldPassword":"…","newPassword":"…"}' ``` Existing JWT remains valid until expiry; if you want to invalidate all sessions, contact `hello@cardzero.ai` (admin-only operation today). ## Public lookups (no auth) Several endpoints require no auth — by design, the data is public on-chain anyway: - `GET /v1/reputation/{walletAddress}` — agent score - `GET /v1/reputation/{walletAddress}/events` — event history - `GET /v1/jobs/{jobId}` — Job state - `GET /v1/jobs/{jobId}/spec` — Job specification JSON - `GET /v1/payments/{paymentId}` — payment status (ID is unguessable) - `GET /v1/catalog` — service catalog - `GET /.well-known/agent/{walletAddress}` — agent profile These are rate-limited per IP (60/min for reputation, 120/min for well-known) to prevent crawler abuse. ## Cross-mode endpoints A few endpoints accept **either** JWT or API Key: - `GET /v1/wallets/:id/balance` - `GET /v1/wallets/:id/payments` Owner uses JWT (Dashboard); agent uses API Key. Same data, same response. ## Security best practices - **Don't put API keys in source control.** Use environment variables. - **Don't share JWT cookies across services.** Each user's session is per-device. - **Rotate keys on team changes.** When someone leaves, rotate any keys they had access to. - **Set tight spending rules + whitelist.** Even with a leaked key, rules constrain damage. ## Rate limits | Limiter | Window | Max | Key | | --- | --- | --- | --- | | `walletCreateLimiter` | 1h | 50 | IP | | `authLimiter` | 1h | 10 | IP | | `paymentLimiter` | 1m | 60 | API Key prefix | | `onrampLimiter` | 1h | 10 | JWT user ID | | `reputationLimiter` | 1m | 60 | IP | | `wellKnownLimiter` | 1m | 120 | IP | Exceeding the limit returns HTTP 429 with a `Retry-After` header. See [Rate limits reference →](/reference/rate-limits) --- ## Wallets URL: https://cardzero.ai/docs/api-reference/wallets > Create, claim, configure, freeze, rename, and rotate keys. # Wallets Base URL: `https://api.cardzero.ai/v1` ## Create wallet No authentication. Rate-limited at 50/hour per IP. **Body:** ```json { "name": "My Agent", "version": "v3" } ``` | Field | Type | Required | Default | | --- | --- | --- | --- | | `name` | string | no | auto-generated | | `version` | "v2" \| "v3" | no | "v2" | **Response 201:** ```json { "id": "wallet_7370ee785775", "chainAddress": "0xa1f2…70D0", "claimKey": "czk_a1b2c3d4e5f6g7h8_…", "name": "My Agent", "status": "pending", "version": "v3" } ``` The wallet is **lazy-deployed**: address is real (CREATE2-deterministic), but no contract on-chain yet. Owner claims to deploy. ## Claim wallet No authentication (claim key serves as proof). Rate-limited at 10/hour per IP. **New user (creates account):** ```json { "claimKey": "czk_a1b2c3d4_…", "username": "myname", "password": "MyStrongPass123!" } ``` **Existing user (attaches wallet to logged-in account):** ```json { "claimKey": "czk_a1b2c3d4_…" } ``` (Send with `Authorization: Bearer ` header.) **Response 201:** ```json { "token": "eyJ…", "userId": "user_…", "agentConfig": { "apiKey": "czapi_a1b2c3d4_…", "walletId": "wallet_7370ee785775", "summary": "== CardZero Wallet Summary ==\n…" } } ``` The `summary` is a multi-line block the Owner can paste back to the agent. ## List wallets JWT required. **Response 200:** ```json { "wallets": [ { "id": "wallet_…", "chain_address": "0x…", "name": "My Agent", "status": "active", "frozen": 0, "tx_limit": "1.0", "daily_limit": "5.0", "expires_at": null, "wallet_version": "v3", "created_at": 1715000000 } ] } ``` ## Get wallet JWT + ownership. Returns the same shape as the list endpoint. ## Get balance JWT or API Key. **Response 200:** ```json { "walletId": "wallet_…", "balance": "10.50", "currency": "USDC" } ``` ## Update spending rules JWT + ownership. **Body** (all fields optional): ```json { "txLimit": "1.0", "dailyLimit": "5.0", "whitelist": ["0x1234…", "0x5678…"], "expiresAt": 1735689600 } ``` Pass `"0"` to remove a limit. Pass `[]` to clear the whitelist. ## Rename JWT + ownership. ```json { "name": "New name" } ``` Limits: 1–50 chars. ## Freeze / unfreeze JWT + ownership. Both block until on-chain confirmed (~1–2s). When frozen, all payments + Job operations revert with `WALLET_FROZEN`. ## Get / rotate API key JWT + ownership. `GET` returns the current plaintext key (decrypts from DB). `POST /rotate` invalidates the current key and returns a new one. ```json { "apiKey": "czapi_…", "walletId": "wallet_…" } ``` ## Get / rotate webhook secret JWT + ownership. The webhook secret is per-wallet; used as HMAC-SHA256 key for outgoing webhook signatures. ```json { "webhookSecret": "whsec_…", "walletId": "wallet_…" } ``` See [Verify webhooks](/recipes/verify-webhook) for how to use it. ## Errors | Code | HTTP | When | | --- | --- | --- | | `INVALID_VERSION` | 400 | `version` not "v2" or "v3" | | `V3_NOT_CONFIGURED` | 503 | V3 factory address missing in server env | | `WALLET_NOT_FOUND` | 404 | bad wallet ID | | `FORBIDDEN` | 403 | wallet ID doesn't belong to JWT user | | `WALLET_NOT_ACTIVE` | 400 | wallet is in "pending" status (not claimed yet) | | `INVALID_NAME` | 400 | name length out of [1,50] | | `RATE_LIMITED` | 429 | exceeded `walletCreateLimiter` | --- ## Payments URL: https://cardzero.ai/docs/api-reference/payments > Send USDC; query payment status. # Payments Base URL: `https://api.cardzero.ai/v1` ## Send payment API Key required. Rate-limited at 60/min per API Key. **Body:** ```json { "to": "0xRecipient…", "amount": "1.0", "currency": "USDC", "memo": "optional note", "idempotencyKey": "optional-uuid", "type": "direct" } ``` | Field | Type | Required | Notes | | --- | --- | --- | --- | | `to` | string | yes | EVM address, 0x-prefixed, 40 hex chars | | `amount` | string | yes | Decimal USDC, max 6 decimals (e.g. "1.5", "0.001") | | `currency` | string | yes | Must be "USDC" | | `memo` | string | no | Free-form, ≤ 200 chars | | `idempotencyKey` | string | no | Use to prevent duplicate charges on retry | | `type` | "direct" \| "x402" | no | Default "direct"; "x402" tags the payment for filtering | **Response 200:** ```json { "id": "pay_abc123def456", "txHash": "0x…", "status": "confirmed", "amount": "1.0", "feeAmount": "0.02", "to": "0xRecipient…", "type": "direct" } ``` `status` values: `pending`, `confirmed`, `failed`. The recipient gets the **full** `amount`. The wallet is debited `amount + feeAmount` (e.g. wallet -1.02 USDC, recipient +1.0 USDC, treasury +0.02 USDC). ## Get payment No authentication. Payment IDs are unguessable (random 12-char hex). **Response 200:** ```json { "id": "pay_abc123def456", "wallet_id": "wallet_…", "to_address": "0x…", "amount": "1.0", "type": "direct", "memo": "optional", "status": "confirmed", "tx_hash": "0x…", "fee_amount": "0.02", "fee_recipient": "0x41a4…91da", "created_at": 1715000000 } ``` ## List payments for wallet JWT or API Key. Pagination via `?limit` + `?offset`. **Query params:** | Param | Default | Max | | --- | --- | --- | | `limit` | 20 | 100 | | `offset` | 0 | — | **Response 200:** ```json { "payments": [ /* same shape as Get payment */ ] } ``` ## Errors | Code | HTTP | When | | --- | --- | --- | | `UNSUPPORTED_CURRENCY` | 400 | currency != "USDC" | | `INVALID_AMOUNT` | 400 | amount format invalid or ≤ 0 | | `INVALID_ADDRESS` | 400 | recipient address malformed | | `WALLET_BUSY` | 409 | another payment in progress for same wallet | | `INSUFFICIENT_BALANCE` | 400 | wallet doesn't have enough USDC | | `WALLET_FROZEN` | 400 | wallet is frozen | | `TX_LIMIT_EXCEEDED` | 400 | amount exceeds per-tx limit | | `DAILY_LIMIT_EXCEEDED` | 400 | sum of today's payments would exceed daily limit | | `NOT_IN_WHITELIST` | 400 | recipient not in whitelist (when whitelist is non-empty) | | `WALLET_EXPIRED` | 400 | past wallet's expiresAt | | `INVALID_API_KEY` | 401 | API key not recognized | Idempotency: if you send the same `idempotencyKey` twice for the same wallet, the second call returns the **first** payment's record (no double-charge). --- ## Jobs (escrow) URL: https://cardzero.ai/docs/api-reference/jobs > ERC-8183 Job lifecycle endpoints — create, fund, submit, refund. # Jobs (escrow) All Job operations require a **V3 wallet** (created with `version: "v3"`). V2 wallets cannot participate in Jobs because their session-key policy doesn't allow Jobs contract calls. Base URL: `https://api.cardzero.ai/v1` ## Create Job API Key (Client side). Rate-limited at 60/min per key. **Body:** ```json { "providerAddress": "0xProvider…", "providerWalletId": "wallet_…", "budgetUsdc": "10000000", "expiredAt": 1735689600, "title": "Translate report", "description": "Output JSON {translated: string} matching schema X.", "evaluatorRule": { "type": "http_check", "url": "https://provider.example/output.json", "expectedStatus": 200, "timeoutMs": 5000 }, "idempotencyKey": "optional-uuid" } ``` | Field | Type | Required | Notes | | --- | --- | --- | --- | | `providerAddress` | string | yes | EVM address | | `providerWalletId` | string | no | If Provider is a CardZero wallet, set this for tighter coupling | | `budgetUsdc` | string | yes | microUSDC ("10000000" = 10 USDC). BigInt-as-string | | `expiredAt` | number | yes | Unix seconds; min 1 day from now | | `title` | string | yes | ≤ 200 chars | | `description` | string | yes | ≤ 2000 chars | | `evaluatorRule.type` | string | yes | "manual" \| "json_schema" \| "http_check" | | `evaluatorRule.url` | string | for http_check | URL to fetch on submit | | `evaluatorRule.expectedStatus` | number | for http_check | Default 200 | | `evaluatorRule.timeoutMs` | number | for http_check | Default 5000 | | `evaluatorRule.schema` | object | for json_schema | (MVP: not yet validated) | | `idempotencyKey` | string | no | Replay-safe | **Response 201:** ```json { "jobId": "job_abc123def456", "onchainJobId": 1, "metadataHash": "0x…", "createTxHash": "0x…" } ``` The Job is now in `open` state. Funds are not yet locked. ## Fund Job API Key (Client side). Rate-limited. No body. Triggers two on-chain UserOps: 1. `USDC.approve(Jobs, budgetUsdc)` 2. `Jobs.fund(jobId)` V3 wallet's policy caps `approve` at 1000 USDC per call. For larger jobs, split or contact us for an alternative path. **Response 200:** ```json { "status": "funded", "fundTxHash": "0x…" } ``` State: `open` → `funded`. ## Submit deliverable API Key (Provider side, must match `job.provider_wallet_id`). **Body:** ```json { "contentHash": "0x…", "contentURI": "https://provider.example/result" } ``` | Field | Type | Required | Notes | | --- | --- | --- | --- | | `contentHash` | string | yes | keccak256 of canonical content (32-byte hex, 0x-prefixed) | | `contentURI` | string | no | Where the deliverable can be fetched | **Response 200:** ```json { "status": "submitted", "submitTxHash": "0x…" } ``` State: `funded` → `submitted`. Evaluator will auto-evaluate within 2 minutes. If Provider is **not a CardZero wallet** (no `provider_wallet_id` in DB), this endpoint returns `EXTERNAL_PROVIDER` (HTTP 400). External Providers must call `Jobs.submit(jobId, contentHash)` directly on-chain. ## Claim refund API Key (Client side). Only valid after `expiredAt`. **Response 200:** ```json { "status": "expired", "refundTxHash": "0x…" } ``` State: `funded`/`submitted` → `expired`. 100% of budget refunded to Client. ## Get Job No authentication. Job state is public. **Response 200:** ```json { "id": "job_…", "onchain_job_id": 1, "client_wallet_id": "wallet_…", "provider_wallet_id": "wallet_…", "provider_address": "0x…", "evaluator_address": "0x8157…0E59", "budget_usdc": "10000000", "status": "completed", "expired_at": 1735689600, "metadata_uri": "https://cardzero.ai/.well-known/jobs/job_…/spec.json", "metadata_hash": "0x…", "deliverable_uri": "https://provider.example/result", "deliverable_hash": "0x…", "evaluator_rule_type": "http_check", "evaluation_outcome": "approved", "evaluation_reason": "http_check: 200 OK", "evaluated_by": "cardzero_auto", "evaluated_at": 1715000050, "create_tx_hash": "0x…", "fund_tx_hash": "0x…", "submit_tx_hash": "0x…", "finalize_tx_hash": "0x…", "created_at": 1715000000 } ``` ## Get Job spec No authentication. Returns the canonical spec JSON. ```json { "version": "1.0", "jobId": "job_…", "title": "Translate report", "description": "…", "deliverableSpec": { "format": "json" }, "evaluation": { "type": "http_check", "url": "…" }, "client": { "wallet": "0x…" }, "provider": { "wallet": "0x…" }, "budget": { "amount": "10000000", "currency": "USDC", "decimals": 6 }, "createdAt": 1715000000, "expiredAt": 1735689600 } ``` `keccak256(specJson) === metadataHash` from the Job state. Use this to verify the on-chain commitment matches the spec. ## List Jobs for wallet JWT + ownership. Used by the Dashboard. **Query params:** | Param | Default | Notes | | --- | --- | --- | | `role` | "client" | "client" or "provider" | | `page` | 1 | | | `pageSize` | 20 | Max 100 | **Response 200:** ```json { "jobs": [ /* same as Get Job */ ], "total": 42, "page": 1, "pageSize": 20 } ``` ## Errors | Code | HTTP | When | | --- | --- | --- | | `CLIENT_NOT_FOUND` | 404 | API Key bound to nonexistent wallet | | `CLIENT_NOT_ACTIVE` | 400 | Wallet not yet claimed | | `PROVIDER_NOT_INTERNAL` | 400 | (Removed; external Providers now allowed) | | `INVALID_BUDGET` | 400 | budgetUsdc unparseable or ≤ 0 | | `EXPIRY_TOO_SHORT` | 400 | expiredAt < now + 86400 | | `SOFT_START_CAP` | 400 | Beta-period budget cap (max $100 USDC) | | `JOB_NOT_FOUND` | 404 | bad jobId | | `NOT_CLIENT` | 403 | API key doesn't belong to Job's client | | `NOT_PROVIDER` | 403 | API key doesn't belong to Job's provider | | `EXTERNAL_PROVIDER` | 400 | Provider must submit on-chain directly | | `INVALID_STATUS` | 400 | Action invalid for current Job state | | `NOT_YET_EXPIRED` | 400 | claimRefund called before expiredAt | | `JOB_BUSY` | 409 | Another transition in progress for this Job | | `IDEMPOTENCY_INCOMPLETE` | 409 | Previous create with same key failed mid-flight | | `CHAIN_CREATE_FAILED` | 502 | On-chain createJob reverted; DB row rolled back | --- ## Reputation URL: https://cardzero.ai/docs/api-reference/reputation > Public reputation lookup; sync trigger for Owners. # Reputation Base URL: `https://api.cardzero.ai/v1` ## Get reputation summary No authentication. Rate-limited at 60/min per IP. **Response 200:** ```json { "walletAddress": "0xa1f2…70D0", "agentId": 1, "agentURI": "https://cardzero.ai/.well-known/agent/0xa1f2…70D0", "onchainStatus": "registered", "score": { "total": 23, "success": 28, "failure": -5 }, "counts": { "payments": 14, "jobs": 2, "freezes": 1 }, "firstSeenAt": 1741232400, "lastActiveAt": 1715990123, "trustSignals": { "frozen": false, "lifetimeVolumeUsdc": "47.50" } } ``` `onchainStatus` values: - `pending`: registration in progress (not yet confirmed) - `registered`: agent is in IdentityRegistry - `failed`: registration permanently failed; manual intervention needed - `not_found`: never attempted (probably a non-CardZero wallet) If `not_found`, score will be all zeros — the wallet has no CardZero history. ## Get reputation events No authentication. Returns event history. **Query params:** | Param | Default | Max | | --- | --- | --- | | `limit` | 50 | 100 | | `offset` | 0 | — | **Response 200:** ```json { "events": [ { "id": "revt_…", "eventType": "job_completed", "value": 5, "tag1": "job", "tag2": null, "sourceKind": "erc8183_job", "sourceRef": "job_abc123:approved", "onchainTxHash": "0x…", "syncedAt": 1715000050, "createdAt": 1715000000 } ] } ``` `onchainTxHash`/`syncedAt` are null until the daily 04:30 UTC cron syncs the event to ReputationRegistry. Off-chain DB record is immediate. ## Trigger sync (admin) JWT + ownership. Manually triggers the sync cron for a single wallet. Useful for debugging. **Response 200:** ```json { "synced": 3, "skipped": 0, "errors": 0 } ``` ## Public agent profile (well-known) No authentication. ERC-8004 standard well-known endpoint. **Response 200:** ```json { "name": "CardZero agent", "description": "…", "type": "agent", "endpoints": { "api": "https://api.cardzero.ai/v1" }, "owner": { "wallet": "0x…" }, "registeredAt": "2026-05-04T10:23:00Z" } ``` Default profile is auto-generated from CardZero data. Owners can override by hosting their own profile JSON and changing `agentURI` in the registry (admin operation today; self-serve roadmap). ## Errors | Code | HTTP | When | | --- | --- | --- | | `INVALID_ADDRESS` | 400 | Wallet address malformed | | `RATE_LIMITED` | 429 | Exceeded reputationLimiter | --- ## Webhooks URL: https://cardzero.ai/docs/api-reference/webhooks > Event notifications with HMAC-SHA256 signatures. CardZero POSTs JSON to your URL when a wallet's Jobs change state. You verify the signature with the per-wallet webhook secret. ## Configure The Owner sets `webhook_url` on the wallet (currently via direct DB; self-serve endpoint coming). Once set, all subsequent state changes for Jobs the wallet participates in trigger a delivery. **Get the signing secret:** ```bash curl https://api.cardzero.ai/v1/wallets/wallet_…/webhook-secret \ -H "Authorization: Bearer " # → { "webhookSecret": "whsec_…", "walletId": "wallet_…" } ``` The secret is generated at claim time. Lazy-generated for legacy wallets on first GET. **Rotate:** ```bash curl -X POST https://api.cardzero.ai/v1/wallets/wallet_…/webhook-secret/rotate \ -H "Authorization: Bearer " ``` Old secret immediately invalid. ## Delivery format ``` POST {wallet.webhook_url} Content-Type: application/json User-Agent: CardZero-Webhook/1.0 X-CardZero-Event: job_completed X-CardZero-Signature: sha256= { "type": "job_completed", "jobId": "job_abc123", "onchainJobId": 1, "walletAddress": "0xa1f2…", "status": "completed", "timestamp": 1715000050 } ``` ## Event types | Type | When | | --- | --- | | `job_created` | Job created (Provider notified) | | `job_funded` | Client funded the Job | | `job_submitted` | Provider submitted deliverable | | `job_completed` | Evaluator approved; funds split | | `job_rejected` | Evaluator rejected; refunded | | `job_expired` | Past expiry; refunded via claimRefund | ## Verify the signature ```typescript import { createHmac, timingSafeEqual } from "crypto"; function verify(rawBody: string, signatureHeader: string, secret: string): boolean { const expected = createHmac("sha256", secret) .update(rawBody) .digest("hex"); const received = signatureHeader.replace(/^sha256=/, ""); return ( expected.length === received.length && timingSafeEqual(Buffer.from(expected), Buffer.from(received)) ); } // In your webhook handler: const signature = req.headers["x-cardzero-signature"]; const rawBody = req.rawBody; // critical: use the raw, un-parsed body const ok = verify(rawBody, signature, process.env.CARDZERO_WEBHOOK_SECRET); if (!ok) return res.status(401).send("Invalid signature"); // ... process the event ``` **Critical**: verify against the **raw bytes** of the body, not the parsed JSON. Most frameworks (Express, Next.js) re-serialize JSON differently than what was sent, breaking the hash. In Express: ```typescript app.use("/cardzero-webhook", express.raw({ type: "application/json" })); app.post("/cardzero-webhook", (req, res) => { const rawBody = req.body.toString("utf8"); // verify, then JSON.parse(rawBody) for content }); ``` In Next.js App Router: ```typescript export async function POST(req: Request) { const rawBody = await req.text(); // verify, then JSON.parse(rawBody) } ``` ## Retry policy CardZero retries failed deliveries up to 3 times with exponential backoff: | Attempt | Backoff | | --- | --- | | 1 | immediate | | 2 | 5 seconds | | 3 | 30 seconds | | 4 | 120 seconds | After 3 failed attempts → `status='failed'` permanently. The event is **not re-tried** — your webhook handler should be reliable. A delivery is considered successful if your endpoint returns HTTP 2xx within the 5-second timeout. Any other status (3xx, 4xx, 5xx) or timeout counts as a failure. ## Best practices - **Respond fast** (≤ 1s ideal). Process the event asynchronously. - **Idempotent processing**: the same event might be delivered twice if your endpoint timed out on the first call. Use `jobId + type` as a dedup key. - **Verify signatures** on every request. Don't trust the source IP. - **Don't expose secrets in logs.** The `X-CardZero-Signature` header is fine to log; the wallet's secret is not. - **Drain queue idempotently**: at-least-once delivery is the model. ## Webhook tester (coming soon) A tool at `cardzero.ai/dashboard/webhooks` to send a test webhook to your URL is on the roadmap. For now, manually call your endpoint with mock data to test signature verification. ## Errors | Code | HTTP | When | | --- | --- | --- | | `WEBHOOK_NO_SECRET` | 400 | Wallet has no webhook_secret yet (lazy-generate by hitting the GET endpoint) | --- # Recipes ## Pay an x402 paywall URL: https://cardzero.ai/docs/recipes/pay-x402-paywall > Your agent calls a paywalled API. The server returns 402. CardZero pays. The agent retries. # Pay an x402 paywall This recipe shows the full handshake when an agent calls an x402-protected HTTP endpoint. ## The flow ``` Agent CardZero API Server │ │ │ ├─ GET /data ─────────────────────────▶ │ │ │ │ │ ◀──────────── 402 Payment Required ──┤ │ (X-PAYMENT-REQUIRED header) │ │ │ │ ├─ POST /x402/pay ▶│ │ │ (url, max, recipient, network) │ │ ├── USDC.transfer ─▶ chain │ │ (UserOp) │ │ ◀── paymentHeader ┤ │ │ │ │ ├─ GET /data ─────────────────────────▶ │ │ X-PAYMENT:
│ │ │ │ │ ◀────────── 200 OK + data ────────────┤ ``` ## Step-by-step code ```typescript const TARGET = "https://example.com/api/expensive-resource"; const API_KEY = process.env.CARDZERO_API_KEY!; async function fetchWithX402(url: string): Promise { // 1. Try without payment first let response = await fetch(url); if (response.status !== 402) { return response; // Either it was free or it failed for some other reason } // 2. Parse payment requirements from headers const requirements = parseX402Headers(response.headers); // Headers from x402 spec: // X-Payment-Required-Network: eip155:8453 // X-Payment-Required-Asset: 0x833589fC...2913 // X-Payment-Required-To: 0xMerchantAddress // X-Payment-Required-Amount: 500000 (0.50 USDC in microUSDC) // 3. Get a payment header from CardZero const payRes = await fetch("https://api.cardzero.ai/v1/x402/pay", { method: "POST", headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url, maxAmount: "1.0", // Cap at 1 USDC; reject if more recipient: requirements.to, network: requirements.network, asset: requirements.asset, idempotencyKey: `x402-${url}-${Date.now()}`, }), }); if (!payRes.ok) throw new Error(`CardZero x402 failed: ${await payRes.text()}`); const { paymentHeader } = await payRes.json(); // 4. Retry with payment proof response = await fetch(url, { headers: { "X-PAYMENT": paymentHeader }, }); return response; } // Use it const data = await fetchWithX402(TARGET).then(r => r.json()); console.log(data); ``` ## Helper: parse x402 headers ```typescript function parseX402Headers(h: Headers): { network: string; asset: string; to: string; amount: string; } { return { network: h.get("x-payment-required-network") || "eip155:8453", asset: h.get("x-payment-required-asset") || "", to: h.get("x-payment-required-to") || "", amount: h.get("x-payment-required-amount") || "0", }; } ``` ## Common pitfalls - **Wrong network** — `eip155:8453` is Base mainnet. If the server demands a different chain, CardZero v1 can't pay (Base only). Returns `UNSUPPORTED_NETWORK`. - **Wrong asset** — must be Base USDC `0x833589fC…2913`. Other tokens not supported. - **`maxAmount` too low** — if the server demands more than your `maxAmount`, CardZero returns `EXCEEDS_MAX_AMOUNT`. Set `maxAmount` based on what you'd actually pay. - **Idempotency** — use a unique `idempotencyKey` per logical request. Replay with the same key returns the prior payment, no double-charge. ## Spending rule interplay The wallet's policy is enforced **on top of** x402: - Per-tx limit applies. If the server demands $5 and your wallet's per-tx limit is $1, the payment reverts with `TX_LIMIT_EXCEEDED`. - Daily limit applies (across all payments, not just x402). - Whitelist applies. Add the merchant address to the whitelist before attempting x402 with strict whitelisting enabled. ## Server side: accepting x402 payments CardZero is the **client side** of x402. To run an x402 server: - Use Coinbase's [`x402` open-source libraries](https://github.com/coinbase/x402) to demand payment in your HTTP responses. - Verify payment proofs server-side (you don't need CardZero for this). - Settle: the on-chain USDC.transfer is already in your account when you see the `X-PAYMENT` header (it's a signed `transferWithAuthorization`). [x402 protocol docs →](https://x402.org) --- ## Hire another agent URL: https://cardzero.ai/docs/recipes/hire-another-agent > Full ERC-8183 Job lifecycle: create, fund, submit, evaluate, settle. # Hire another agent This recipe walks through hiring another agent under on-chain escrow. Result: budget locks until the work is delivered and approved. ## Prerequisites - **Both agents have V3 wallets** (see [Quickstart](/getting-started/quickstart)). - **Client wallet has USDC** to cover budget + fees. - **Provider's wallet address** is known to Client. ## End-to-end example We'll hire a translation agent for $1 USDC. ### 1. Client creates the Job ```typescript const CLIENT_KEY = process.env.CARDZERO_API_KEY!; const PROVIDER_ADDR = "0xProviderWalletAddress…"; const create = await fetch("https://api.cardzero.ai/v1/jobs", { method: "POST", headers: { "Authorization": `Bearer ${CLIENT_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ providerAddress: PROVIDER_ADDR, budgetUsdc: "1000000", // 1 USDC in microUSDC expiredAt: Math.floor(Date.now() / 1000) + 86400 + 3600, // 1d 1h from now title: "Translate manifest.json to Japanese", description: "Input: https://example.com/manifest.json. " + "Output JSON {translated: object}. Match input structure.", evaluatorRule: { type: "http_check", url: "https://provider.example/translation-output", expectedStatus: 200, timeoutMs: 5000, }, idempotencyKey: `hire-translator-${Date.now()}`, }), }).then(r => r.json()); console.log("Job created:", create); // { jobId: "job_…", onchainJobId: 1, metadataHash: "0x…", createTxHash: "0x…" } ``` The Job is **`open`** — no money locked yet. ### 2. Client funds the Job ```typescript const fund = await fetch( `https://api.cardzero.ai/v1/jobs/${create.jobId}/fund`, { method: "POST", headers: { "Authorization": `Bearer ${CLIENT_KEY}` }, }, ).then(r => r.json()); console.log("Funded:", fund); // { status: "funded", fundTxHash: "0x…" } ``` The Job is now **`funded`**. 1 USDC is locked in the Jobs contract escrow. If the Provider has a webhook configured, they get a `job_funded` notification. ### 3. Provider submits a deliverable The Provider does the work, computes a `keccak256` hash of the canonical output (deterministic representation), and submits: ```typescript import { keccak256, toBytes } from "viem"; const PROVIDER_KEY = process.env.PROVIDER_API_KEY!; const result = await translateAndCanonicalize(...); const contentHash = keccak256(toBytes(JSON.stringify(result))); const submit = await fetch( `https://api.cardzero.ai/v1/jobs/${jobId}/submit`, { method: "POST", headers: { "Authorization": `Bearer ${PROVIDER_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ contentHash, contentURI: "https://provider.example/translation-output", }), }, ).then(r => r.json()); // { status: "submitted", submitTxHash: "0x…" } ``` The Job is now **`submitted`**. Evaluator + Client get webhooks. ### 4. Evaluator runs (CardZero's auto-evaluator) CardZero's Evaluator cron runs every 2 minutes. For our `http_check` rule: 1. Fetches `rule.url` (the Provider's output URL). 2. Checks status === 200. 3. If pass → `Jobs.complete(jobId, reason)`. 4. If fail → `Jobs.reject(jobId, reason)`. Result: ~2 minutes after `submit`, the Job transitions to `completed` or `rejected`. ### 5. Settlement (automatic on `complete`) The contract auto-splits the budget: | Recipient | Amount | Of | | --- | --- | --- | | Provider wallet | 0.93 USDC | 93% | | Evaluator EOA | 0.05 USDC | 5% | | CardZero treasury | 0.02 USDC | 2% | | Total | 1.00 USDC | 100% | All in a single on-chain transaction. The Provider's wallet shows `+0.93 USDC`. The deal is done. If rejected: 100% refund to Client, 0 to anyone else. ## Verifying after the fact Anyone (you, the Provider, an auditor) can verify the Job's full state: ```bash curl https://api.cardzero.ai/v1/jobs/job_… ``` Returns all 4 lifecycle tx hashes. Click any on Basescan to see the on-chain trace. ## Common pitfalls ### "JOB_BUSY" You called fund or submit while another transition is in flight. CardZero serializes per-Job operations via DB lock. Wait a few seconds, retry. ### "EXTERNAL_PROVIDER" You called `/submit` but the Provider isn't a CardZero wallet. Submission must be done by the Provider's own infrastructure via direct on-chain call: ```typescript import { writeContract } from "viem"; await writeContract({ address: "0xb28a0cca…4ba8", // Jobs contract abi: jobsAbi, functionName: "submit", args: [BigInt(onchainJobId), contentHash], }); ``` ### "EXPIRY_TOO_SHORT" `expiredAt` must be at least 1 day in the future. Contract enforces this to prevent grief attacks (instant-expiry abuse). ### Soft-start budget cap ($100) During beta, jobs above $100 USDC are rejected with `SOFT_START_CAP`. Lifted once we exit beta. ## Choosing an evaluator rule | Use case | Rule type | | --- | --- | | You want to manually approve | `manual` (admin clicks button to finalize) | | Output goes to a known URL | `http_check` (fetch URL, compare status/body) | | Output structure is JSON Schema | `json_schema` (MVP: presence-check only) | For complex rules (semantic similarity, LLM judging), the manual flow is the safest. CardZero may add ML-based evaluators in the future. ## Reputation reflection Job completion writes a `+5` reputation event for the Provider. After a few completions, their public reputation card at `cardzero.ai/agent/{address}` shows the track record — useful for future Clients to vet them. Failed jobs write `-3`. Reputation isn't free. [Reputation concept →](/concepts/reputation) --- ## Verify a webhook signature URL: https://cardzero.ai/docs/recipes/verify-webhook > HMAC-SHA256 in 5 languages. Don't trust the source IP — verify. # Verify a webhook signature CardZero signs every webhook with HMAC-SHA256 over the **raw request body**, using your wallet's per-wallet `webhook_secret`. Verifying is a few lines of code. ## Get your secret ```bash curl https://api.cardzero.ai/v1/wallets/wallet_…/webhook-secret \ -H "Authorization: Bearer " ``` Save the `webhookSecret` value (starts with `whsec_`) in your server's secrets. If you ever leak it, rotate: ```bash curl -X POST https://api.cardzero.ai/v1/wallets/wallet_…/webhook-secret/rotate \ -H "Authorization: Bearer " ``` ## Headers we send ``` X-CardZero-Event: job_completed X-CardZero-Signature: sha256=a1b2c3d4... User-Agent: CardZero-Webhook/1.0 Content-Type: application/json ``` ## Critical: use the raw body HMAC is over **bytes**, not parsed JSON. If your framework auto-parses, you must access the original raw bytes for verification. Common pitfalls: - ❌ `JSON.stringify(req.body)` — doesn't match the original bytes (key order, whitespace). - ✅ Use `express.raw()` middleware OR read `req.text()` in Next.js. ## Examples ```typescript import express from "express"; import { createHmac, timingSafeEqual } from "crypto"; const app = express(); // CRITICAL: raw body parser BEFORE any json() middleware app.use("/webhooks/cardzero", express.raw({ type: "application/json" })); app.post("/webhooks/cardzero", (req, res) => { const rawBody = (req.body as Buffer).toString("utf8"); const sigHeader = req.headers["x-cardzero-signature"] as string; if (!verify(rawBody, sigHeader, process.env.WEBHOOK_SECRET!)) { return res.status(401).send("invalid signature"); } const event = JSON.parse(rawBody); console.log("Got CardZero event:", event.type, event.jobId); // ... process event ... res.status(200).send("ok"); }); function verify(body: string, header: string, secret: string): boolean { const expected = createHmac("sha256", secret).update(body).digest("hex"); const received = header.replace(/^sha256=/, ""); if (expected.length !== received.length) return false; return timingSafeEqual(Buffer.from(expected), Buffer.from(received)); } ``` ```typescript // app/api/webhooks/cardzero/route.ts import { createHmac, timingSafeEqual } from "crypto"; export async function POST(req: Request) { const rawBody = await req.text(); const sigHeader = req.headers.get("x-cardzero-signature") || ""; if (!verify(rawBody, sigHeader, process.env.WEBHOOK_SECRET!)) { return new Response("invalid signature", { status: 401 }); } const event = JSON.parse(rawBody); // ... process ... return new Response("ok", { status: 200 }); } function verify(body: string, header: string, secret: string): boolean { const expected = createHmac("sha256", secret).update(body).digest("hex"); const received = header.replace(/^sha256=/, ""); if (expected.length !== received.length) return false; return timingSafeEqual(Buffer.from(expected), Buffer.from(received)); } ``` ```python import hmac import hashlib from flask import Flask, request app = Flask(__name__) WEBHOOK_SECRET = os.environ["CARDZERO_WEBHOOK_SECRET"] @app.route("/webhooks/cardzero", methods=["POST"]) def webhook(): raw_body = request.get_data() # bytes signature = request.headers.get("X-CardZero-Signature", "") expected = hmac.new( WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256 ).hexdigest() received = signature.replace("sha256=", "", 1) if not hmac.compare_digest(expected, received): return "invalid signature", 401 event = json.loads(raw_body) # ... process ... return "ok", 200 ``` ```go package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "os" "strings" ) var secret = os.Getenv("CARDZERO_WEBHOOK_SECRET") func webhookHandler(w http.ResponseWriter, r *http.Request) { rawBody, _ := io.ReadAll(r.Body) sigHeader := r.Header.Get("X-CardZero-Signature") received := strings.TrimPrefix(sigHeader, "sha256=") mac := hmac.New(sha256.New, []byte(secret)) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(received)) { http.Error(w, "invalid signature", 401) return } // ... process rawBody as JSON ... w.WriteHeader(http.StatusOK) } ``` ```ruby require 'openssl' class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token def cardzero raw_body = request.raw_post sig_header = request.headers['X-CardZero-Signature'] expected = OpenSSL::HMAC.hexdigest('SHA256', ENV['WEBHOOK_SECRET'], raw_body) received = sig_header.sub('sha256=', '') unless ActiveSupport::SecurityUtils.secure_compare(expected, received) render plain: 'invalid signature', status: 401 return end event = JSON.parse(raw_body) # ... process ... head :ok end end ``` ## What to do after verifying Process the event idempotently. Each event has a stable `jobId + type` — de-dup using that. Don't process the same `(jobId, type)` twice. Example dedup: ```typescript const dedupKey = `${event.jobId}-${event.type}`; const wasProcessed = await redis.set(`processed:${dedupKey}`, "1", "EX", 86400, "NX"); if (!wasProcessed) { console.log("Duplicate event, skipping:", dedupKey); return res.status(200).send("ok"); } // ... actually do the work ... ``` ## Test before going live Manually trigger a webhook by causing a state change (create a Job, fund it). Check your endpoint logs to confirm: 1. Signature verifies ✅ 2. Event JSON parses ✅ 3. Your handler runs ✅ 4. Returns HTTP 2xx ✅ CardZero will retry up to 3 times with backoff. Failed deliveries are logged in your Owner dashboard. [Webhook API reference →](/api-reference/webhooks) --- ## Vet a counterparty URL: https://cardzero.ai/docs/recipes/monitor-reputation > Use on-chain reputation + identity to decide who to transact with. # Vet a counterparty Before sending USDC to an unknown wallet — or hiring an unknown agent — check their CardZero reputation. Two API calls, all public. ## When to vet | Situation | What to check | | --- | --- | | Hiring a Provider for an A2A Job | Reputation score > X, recent activity, no recent freezes | | Sending direct payment to a new address | Registered as a CardZero agent at all? | | Listing in an aggregator | Match against allow/deny list | | Suspicious activity from a counterparty | Look at recent reputation events | ## The two endpoints ### Summary (one number, fast) ```bash curl https://api.cardzero.ai/v1/reputation/0xCounterparty… ``` Returns score, counts, registration status. ~50ms response. ### Event history (detailed) ```bash curl https://api.cardzero.ai/v1/reputation/0xCounterparty…/events?limit=20 ``` Returns the last 20 reputation events with timestamps + on-chain tx hashes. ## Decision logic example ```typescript async function shouldTransact(counterpartyAddress: string): Promise { const rep = await fetch( `https://api.cardzero.ai/v1/reputation/${counterpartyAddress}`, ).then(r => r.json()); // 1. Must be a registered CardZero agent if (rep.onchainStatus !== "registered") { console.log("Not a CardZero agent — cannot vet"); return false; } // 2. Not currently frozen if (rep.trustSignals.frozen) { console.log("Wallet is frozen by Owner — declining"); return false; } // 3. Minimum score if (rep.score.total < 5) { console.log("Score too low:", rep.score.total); return false; } // 4. Recent activity (within 30 days) const now = Math.floor(Date.now() / 1000); if (rep.lastActiveAt && now - rep.lastActiveAt > 30 * 86400) { console.log("Inactive for >30 days, treating as cold"); return false; } // 5. Some volume (filters out fresh accounts) const volume = parseFloat(rep.trustSignals.lifetimeVolumeUsdc); if (volume < 1.0) { console.log("Lifetime volume too low:", volume); return false; } return true; } ``` ## Stratified thresholds by transaction value For high-value Jobs, demand stricter reputation: ```typescript function reputationRequirements(usdcAmount: number) { if (usdcAmount <= 1) return { minScore: 0, minVolume: 0 }; if (usdcAmount <= 10) return { minScore: 5, minVolume: 1 }; if (usdcAmount <= 100) return { minScore: 20, minVolume: 50 }; return { minScore: 50, minVolume: 500 }; } ``` ## Recent failures pattern A counterparty might have a high lifetime score but recent failures. Pull recent events: ```typescript const events = await fetch( `https://api.cardzero.ai/v1/reputation/${addr}/events?limit=10`, ).then(r => r.json()); const recentFails = events.events.filter( e => e.value < 0 && (Date.now() / 1000 - e.createdAt) < 7 * 86400, ); if (recentFails.length >= 3) { console.log("3+ failures in last 7 days, declining"); return false; } ``` ## Identity verification Beyond reputation, you can verify identity: ```typescript // Fetch the agent's profile const profileURL = rep.agentURI; const profile = await fetch(profileURL).then(r => r.json()); // Verify it's a real CardZero registration if (profile.owner.wallet.toLowerCase() !== counterpartyAddress.toLowerCase()) { console.log("Profile owner mismatch — fishy"); return false; } // Optional: verify operator's claimed identity (off-chain) console.log("Operator:", profile.operator); ``` ## Visualizing for humans Embed this in your UI so users can review a counterparty before approving: ```tsx async function ReputationCard({ address }: { address: string }) { const rep = await fetch(`https://api.cardzero.ai/v1/reputation/${address}`).then(r => r.json()); return (
Score: {rep.score.total}
Lifetime: {rep.trustSignals.lifetimeVolumeUsdc} USDC
Payments: {rep.counts.payments} · Jobs: {rep.counts.jobs}
Full profile →
); } ``` ## Reputation isn't a guarantee A high score is **evidence**, not **proof**. It tells you the counterparty has behaved well in the past. It can't predict future behavior with certainty. Use reputation **plus** other signals: - Direct conversation / out-of-band verification - Off-chain context (their GitHub, their org's website) - Smart contract terms (escrow + Evaluator can backstop) - Diversification (don't put $10K with one new agent) [Reputation concept →](/concepts/reputation) --- # Reference ## Error codes URL: https://cardzero.ai/docs/reference/error-codes > Every error CardZero can return, what it means, and how to recover. # Error codes CardZero errors have a stable `code` (machine-readable) plus a `message` (human). HTTP status indicates the category. Check `code` for programmatic dispatch. ## Format ```json { "error": "INSUFFICIENT_BALANCE", "message": "Wallet balance too low for this payment" } ``` ## Authentication errors (401, 403) | Code | When | Recover | | --- | --- | --- | | `UNAUTHORIZED` | Missing / malformed `Authorization` header | Check format `Bearer ` | | `INVALID_API_KEY` | API key not recognized or revoked | Get new key from Owner | | `API_KEY_NOT_BOUND_TO_WALLET` | API key exists but has no wallet | Re-issue via /api-key/rotate | | `FORBIDDEN` | Action not allowed for this auth | Check ownership / role | ## Validation errors (400) | Code | When | Recover | | --- | --- | --- | | `INVALID_VERSION` | wallet version not "v2" or "v3" | Use a valid value | | `INVALID_AMOUNT` | amount format wrong or ≤ 0 | Use decimal string, e.g. "1.0" | | `INVALID_ADDRESS` | EVM address malformed | 0x + 40 hex chars | | `INVALID_BUDGET` | budgetUsdc unparseable | BigInt-safe string in microUSDC | | `INVALID_TITLE` | title > 200 chars or wrong type | Shorten | | `INVALID_DESCRIPTION` | description > 2000 chars | Shorten | | `INVALID_EVALUATOR_RULE` | rule.type not in `{manual, json_schema, http_check}` | Use valid type | | `INVALID_HASH` | contentHash not 32-byte hex | keccak256 + 0x prefix + 64 hex chars | | `INVALID_NAME` | name length out of [1,50] | Trim | | `INVALID_USERNAME` | username out of [3,20] chars | Adjust | | `EXPIRY_TOO_SHORT` | expiredAt < now + 86400 | Set ≥ 1 day in future | | `MISSING_*` | Required field absent | Add the field | ## Resource errors (404, 409) | Code | When | Recover | | --- | --- | --- | | `WALLET_NOT_FOUND` | wallet ID doesn't exist | Check ID | | `JOB_NOT_FOUND` | jobId doesn't exist | Check ID | | `PAYMENT_NOT_FOUND` | paymentId doesn't exist | Check ID | | `USER_NOT_FOUND` | user ID invalid | Check ID | | `WALLET_BUSY` | Another payment in flight for same wallet | Retry in a few seconds | | `JOB_BUSY` | Another transition in flight for same Job | Retry in a few seconds | | `WALLET_NOT_ACTIVE` | Wallet still in "pending" state | Owner must claim first | | `CLAIM_IN_PROGRESS` | Concurrent claim attempt | Wait 5 minutes; auto-recovers | | `CLAIM_KEY_INVALID` | Bad / expired / used key | Generate a new claim key (POST /wallets) | | `IDEMPOTENCY_INCOMPLETE` | Same idempotencyKey used; previous attempt mid-failed | Manual recovery; contact support | ## Spending policy errors (400) Enforced **on-chain**; no API workaround. | Code | When | Recover | | --- | --- | --- | | `INSUFFICIENT_BALANCE` | Wallet doesn't have enough USDC | Owner funds wallet | | `WALLET_FROZEN` | Owner froze the wallet | Owner unfreezes | | `TX_LIMIT_EXCEEDED` | Single payment > per-tx limit | Split payment or Owner raises limit | | `DAILY_LIMIT_EXCEEDED` | Total today > daily limit | Wait for UTC midnight or Owner raises limit | | `NOT_IN_WHITELIST` | Recipient not in whitelist (when set) | Owner adds to whitelist | | `WALLET_EXPIRED` | Past wallet's expiresAt | Owner extends expiry | ## Job-specific errors (400, 403) | Code | When | Recover | | --- | --- | --- | | `CLIENT_NOT_FOUND` | API key not bound to a wallet | Re-issue API key | | `CLIENT_NOT_ACTIVE` | Client wallet not yet claimed | Claim first | | `NOT_CLIENT` | API key's wallet ≠ Job's client | Use correct API key | | `NOT_PROVIDER` | API key's wallet ≠ Job's provider | Use correct API key | | `EXTERNAL_PROVIDER` | Provider must submit on-chain directly | Use Provider's own infra | | `INVALID_STATUS` | Job state doesn't allow this action | Check Job state | | `NOT_YET_EXPIRED` | claimRefund called too early | Wait until expiredAt | | `SOFT_START_CAP` | Beta budget cap (max $100 USDC) | Wait for cap removal | | `SELF_JOB` | Client and Provider are same wallet | Use a different Provider | ## Chain errors (502) | Code | When | Recover | | --- | --- | --- | | `CHAIN_CREATE_FAILED` | On-chain createJob reverted | Investigate via Basescan; DB row already rolled back, retry safe | | `NOT_ONCHAIN` | DB has Job, but chain doesn't (rare race) | Manual cleanup; contact support | | `RPC_TIMEOUT` | Blockchain RPC didn't respond in 60s | Retry; CardZero falls back to backup RPC | ## Configuration errors (500, 503) | Code | When | Recover | | --- | --- | --- | | `V3_NOT_CONFIGURED` | V3 factory address missing in server env | Server-side; report to ops | | `EVALUATOR_UNCONFIGURED` | Evaluator EOA missing | Server-side | | `INTERNAL` | Generic 500 | Retry; report if persistent | ## Rate limiting (429) | Code | When | Recover | | --- | --- | --- | | `RATE_LIMITED` | Exceeded rate limiter | Wait for window reset (`Retry-After` header) | ## Webhook errors | Code | When | Recover | | --- | --- | --- | | `WEBHOOK_NO_SECRET` | Wallet has no webhook_secret | Hit `GET /webhook-secret` once to lazy-generate | ## Onramp errors | Code | When | Recover | | --- | --- | --- | | `CDP_NOT_CONFIGURED` | Coinbase Onramp credentials missing | Server-side | | `INVALID_WALLET` | Wallet doesn't belong to user | Check ownership | ## Decision tree for callers ``` HTTP 401/403 → re-auth HTTP 400 + INVALID_* → fix request HTTP 400 + spending policy → Owner action needed HTTP 404 → check IDs HTTP 409 → retry with backoff (idempotency-safe) HTTP 429 → honor Retry-After HTTP 502/503 → retry with backoff (downstream issue) HTTP 500 → retry once, then escalate ``` ## When in doubt Check the response body's `message` for human-readable detail. Most messages include the specific value that failed validation. If the error doesn't match anything above, it might be a new code — file an issue at [github.com/mrocker/CardZero](https://github.com/mrocker/CardZero/issues). --- ## Rate limits URL: https://cardzero.ai/docs/reference/rate-limits > Per-endpoint quota table; how to handle 429 responses. # Rate limits CardZero rate-limits at the API layer using `express-rate-limit` keyed on either IP (CF-Connecting-IP from Cloudflare) or API Key prefix. Limits are **per-instance** (we run a single process today; no distributed limits). ## The table | Limiter | Window | Max | Key | Endpoints | | --- | --- | --- | --- | --- | | `walletCreateLimiter` | 1 hour | 50 | IP | `POST /v1/wallets` | | `authLimiter` | 1 hour | 10 | IP | `POST /v1/auth/login`, `/v1/auth/claim`, `/v1/auth/change-password` | | `paymentLimiter` | 1 minute | 60 | API Key prefix (or IP if no key) | `POST /v1/payments`, `POST /v1/x402/pay`, all 4 `POST /v1/jobs/*` | | `onrampLimiter` | 1 hour | 10 | JWT user ID (or IP) | `POST /v1/onramp/session-token` | | `reputationLimiter` | 1 minute | 60 | IP | `GET /v1/reputation/*` | | `wellKnownLimiter` | 1 minute | 120 | IP | `GET /.well-known/agent/*`, `GET /v1/catalog` | ## Response headers When you hit a limited endpoint, every response includes: ``` RateLimit-Policy: 60;w=60 RateLimit-Limit: 60 RateLimit-Remaining: 47 RateLimit-Reset: 30 ``` `RateLimit-Reset` is **seconds until window reset**. After exceeding: ``` HTTP/1.1 429 Too Many Requests Retry-After: 23 Content-Type: application/json { "error": "RATE_LIMITED", "message": "Too many payment requests. Try again later." } ``` Honor `Retry-After`. Don't busy-loop. ## Rationale per limit **`walletCreateLimiter`** — 50/hour per IP is generous (one IP creating 50 wallets/hour is unusual but legitimate for testing). Wallets are lazy-deployed so 50 cost ~zero on-chain. If you actually need more, contact us. **`authLimiter`** — 10/hour blocks credential stuffing. A real user logs in once a day; even a forgetful user retries < 5 times. **`paymentLimiter`** — 60/minute per API Key. That's 1 payment per second sustained, with bursts allowed. A real agent doing micro-payments at this rate is unusual but legitimate. Lower bar = paymaster cost protection. **`onrampLimiter`** — 10/hour per user. Coinbase Onramp session tokens are short-lived; you don't need many. **`reputationLimiter`** — 60/minute per IP. A scraper could pull reputation data fast; this caps it without blocking real polling use cases. **`wellKnownLimiter`** — 120/minute per IP. Higher because LLMs / search indexers may pull `.well-known/agent/*` aggressively, and that's a feature. ## Handling 429 in code ```typescript async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const res = await fetch(url, options); if (res.status !== 429) return res; const retryAfter = parseInt(res.headers.get("Retry-After") || "60", 10); console.log(`Rate limited, waiting ${retryAfter}s...`); await new Promise(r => setTimeout(r, retryAfter * 1000)); } throw new Error("Max retries reached"); } ``` ## Behind a shared IP? If your agent runs on a corporate egress IP shared with many other CardZero users, you might trip IP-keyed limits faster than expected. The `paymentLimiter` is keyed on **API Key prefix** instead of IP precisely to avoid this — so payments shouldn't be affected. For the auth + wallet-create limiters, contact us if you legitimately need higher quota for a known IP range. ## Known headers from Cloudflare We respect `CF-Connecting-IP` for getting the real client IP behind Cloudflare. We **don't** trust `X-Forwarded-For` (potentially spoofed). If you proxy requests through your own intermediary, you'll see your intermediary's IP in our limits (not your end users'). ## Future: distributed rate limiting Currently single-instance. If we scale to multiple workers, we'll switch to Redis-backed limits to share counts across workers. The behavior will be identical from the client's perspective. ## Errors | Code | HTTP | When | | --- | --- | --- | | `RATE_LIMITED` | 429 | Limit exceeded | --- ## Contract addresses URL: https://cardzero.ai/docs/reference/contract-addresses > All CardZero smart contracts on Base mainnet, with role separation. # Contract addresses All addresses on **Base mainnet (chain ID 8453)**. CardZero is not deployed on Base Sepolia (yet) or any other chain. ## Wallet contracts | Contract | Address | Notes | | --- | --- | --- | | **CardZeroFactory V2** | `0xa3fc38f1b9379ed269a9ac75b6de229fa55e412e` | Deploys V2 wallets (payments only) | | CardZeroWallet V2 (impl) | `0x601b1E85931fa25e2e82B387c829302D56De7470` | UUPS proxy implementation | | **CardZeroFactoryV3** | `0x0c1d37f49ab9da5b6da2e2938be5567fbba4aabb` | Deploys V3 wallets (payments + Jobs) | | CardZeroWalletV3 (impl) | `0x70ff113944ad5dcF11A28B240c8F3244112C2298` | UUPS proxy implementation | | CardZeroFactory V1 (legacy) | `0xebf66b2dfcd8c4f96248ddfedc8f7c49d49f7283` | EIP-1167 minimal proxies; not upgradeable. Legacy users only. | V2 wallets cannot upgrade to V3. Address namespace is different (CREATE2 salt includes "v3" suffix in V3 factory). ## ERC-8004 (Identity + Reputation) | Contract | Address | Notes | | --- | --- | --- | | **IdentityRegistry** | `0x1db9b790ae49def806d3d16172de04d2557fecbe` | UUPS proxy | | IdentityRegistry (impl) | `0x82993dfdb6104849fa1fcbb4139f145ec6d3b8e2` | | | **ReputationRegistry** | `0xc00a5757c63d65005d22e507eae045df5e83b338` | UUPS proxy | | ReputationRegistry (impl) | `0x9805be287464687692006a6d68278a288365f987` | | ## ERC-8183 (Job escrow) | Contract | Address | Notes | | --- | --- | --- | | **CardZeroJobs** | `0xb28a0cca5ac28466f3d175f35b97aa104d4c4ba8` | UUPS proxy | | CardZeroJobs (impl) | `0x5e545d00af169a35a1211fffb25331a2ec694e1f` | | ## External | Contract | Address | Notes | | --- | --- | --- | | **USDC** | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | Native Base USDC, 6 decimals, by Circle | | **EntryPoint** (ERC-4337) | `0x0000000071727De22E5E9d8BAf0edAc6f37da032` | v0.7 | ## Role separation (4 EOAs, 0 overlap) CardZero uses **4 distinct EOAs** with different roles. None of them holds power that any other has. This is deliberate compartmentalization. | EOA | Address | Role | | --- | --- | --- | | **DEPLOYER** | `0x79985809b620F488D524fFA2e29c1377e018edce` | Owns wallets, deploys contracts, signs claim tx, admin role on UUPS upgrades | | **REGISTRAR** | `0xfd865c3C6AbC3F714D587c583166dd096a7EED51` | Registers agents in IdentityRegistry. No payment / wallet authority. | | **ATTESTOR** | `0xf76a7a569060fD800dcfc2c2EEa8a4060385a1D4` | Signs ReputationRegistry events. Bound to `scoringRulesHash`. | | **EVALUATOR** | `0x8157Cb8e28707eD7aeC693662D51563c63620E59` | Calls `Jobs.complete` and `Jobs.reject`. No deploy or registration authority. | If any single key leaks, blast radius is bounded: - Deployer leak: attacker can drain wallet ETH (gas), deploy spam, but cannot move user USDC (still subject to wallet policy). - Registrar leak: attacker can register fake agents but cannot move funds. - Attestor leak: attacker can write bogus reputation events but score is capped at +20 / -10 per event; SCORING-RULES.md hash mismatch detectable. - Evaluator leak: attacker can mis-finalize Jobs but only ones in `submitted` state; cannot steal funds beyond the platform-fee % of in-flight Jobs. ## Treasury | Address | Receives | | --- | --- | | `0x41a45c8fbB03d0137163c55d950B3F93330091da` | Platform fees (2%) on every payment + 2% of Job budgets on completion | This is currently a CardZero-controlled multisig-eligible address. Could move to a multisig in the future without disrupting fee flow. ## Verifying on Basescan Every contract is verified on Basescan with source. Click any address above: - [CardZeroFactory V2](https://basescan.org/address/0xa3fc38f1b9379ed269a9ac75b6de229fa55e412e) - [CardZeroFactoryV3](https://basescan.org/address/0x0c1d37f49ab9da5b6da2e2938be5567fbba4aabb) - [CardZeroJobs](https://basescan.org/address/0xb28a0cca5ac28466f3d175f35b97aa104d4c4ba8) - [IdentityRegistry](https://basescan.org/address/0x1db9b790ae49def806d3d16172de04d2557fecbe) - [ReputationRegistry](https://basescan.org/address/0xc00a5757c63d65005d22e507eae045df5e83b338) ## Direct interaction (bypassing CardZero API) You can interact with these contracts **directly** via any Ethereum tooling (viem, ethers, foundry). The API is convenience; the contracts are the authority. This is a property, not a bug. Example: directly read a Job's state without API: ```typescript import { readContract } from "viem"; const job = await readContract({ address: "0xb28a0cca5ac28466f3d175f35b97aa104d4c4ba8", abi: [/* CardZeroJobs.getJob */], functionName: "getJob", args: [BigInt(jobId)], }); ``` If we ever shut down our API tomorrow, your wallet keeps working — interact directly with the contracts. Reputation events stay queryable from the `ReputationRegistry` indefinitely. ## Live mainnet proof The first 1 USDC end-to-end Job lifecycle: [Basescan: 0xf71ce10c…0593bd](https://basescan.org/tx/0xf71ce10c10eb34fcf10cf8f2e8c1ca03f4b85fdba07c65862bb060db990593bd) 5 transactions: createJob → approve → fund → submit → complete. Total 1 USDC moved through escrow with auto-split 0.93 / 0.05 / 0.02. --- ## Changelog URL: https://cardzero.ai/docs/reference/changelog > What changed, when, and why. # Changelog ## 2026-05-06 — Sprint 9 escrow + pre-launch hardening **Live**: ERC-8183 Job escrow on Base mainnet. - New: `CardZeroJobs` contract (escrow lifecycle: open → funded → submitted → completed/rejected/expired). - New: `CardZeroFactoryV3` + `CardZeroWalletV3` (Jobs-aware wallets). - New: `JobService` API + 4 endpoints (`POST /v1/jobs`, `/fund`, `/submit`, `/refund`). - New: `EvaluatorService` (auto-evaluation rule engine: manual / json_schema / http_check). - New: `WebhookService` with HMAC-SHA256, 3-attempt retry, exponential backoff. - New: Cross-worker DB lock (`job_locks`, `wallet_locks`) replacing in-memory Maps. - New: Per-wallet `webhook_secret` (was: shared fallback string — fixed). - New: Rate limiter on `/v1/jobs/*` endpoints (was: unprotected). - New: Live counter on landing page (`/stats.json`, 5-min cron). - New: SEO infra (robots.txt, sitemap.xml, llms.txt, OpenGraph, JSON-LD). - New: `/privacy` and `/terms` pages. - New: Mintlify documentation site (this site!). - Migration: `wallets.wallet_version` ('v2' default). - Migration: `wallets.webhook_secret` (lazy-generated for legacy wallets). - Migration: `jobs.provider_wallet_id` nullable (allows external Providers). - Audit: 13-dimension pre-launch self-audit ([WEBSITE-AUDIT.md](https://github.com/mrocker/CardZero/blob/main/docs/WEBSITE-AUDIT.md), [PRE-LAUNCH-AUDIT.md](https://github.com/mrocker/CardZero/blob/main/docs/PRE-LAUNCH-AUDIT.md)). - Real mainnet E2E: 1 USDC full Job lifecycle ([tx 0xf71ce10c…](https://basescan.org/tx/0xf71ce10c10eb34fcf10cf8f2e8c1ca03f4b85fdba07c65862bb060db990593bd)). ## 2026-05-04 — Sprint 8 ERC-8004 deployment **Live**: Identity + Reputation registries on Base mainnet. - New: `CardZeroIdentityRegistry` contract (UUPS). - New: `CardZeroReputationRegistry` contract (UUPS, EIP-712 + ERC-1271 dual-path). - New: `ReputationService` API with idempotent event recording, retry queue. - New: Public endpoints: `/v1/reputation/{wa}`, `/.well-known/agent/{wa}`, `/v1/catalog`. - New: Dashboard ReputationCard component. - New: Public agent profile pages at `/agent/{walletAddress}`. - New: `SCORING-RULES.md` published; hash committed on-chain. - New: Daily 04:30 UTC cron syncs unsynced events to chain. - New: 4-EOA role isolation (DEPLOYER / REGISTRAR / ATTESTOR / EVALUATOR). - 3 agents auto-registered, 5 reputation events backfilled from history. ## 2026-03-29 — MCP Server published - `cardzero-mcp@0.1.0` on npm (later 0.2.0 with Job tools). - 6 → 10 tools: create_wallet, get_balance, send_payment, list_payments, pay_x402, get_payment, **+ create_job, fund_job, submit_job, get_job**. - Stdio transport; works in Claude Desktop, Cursor, VS Code, Cline, Continue. ## 2026-03-22 — UUPS upgrade + security audit P0/P1 - Migrated wallets from EIP-1167 minimal proxies to ERC-1967 UUPS upgradeable. - Self-audit produced [`SECURITY-REVIEW.md`](https://github.com/mrocker/CardZero/blob/main/docs/SECURITY-REVIEW.md): 3 critical, 9 high, 12 medium, ~10 low. All P0/P1 fixed. - Notable fixes: BigInt fee math (was floating-point), policy bypass via approve+transferFrom, claim race condition, EVM empty-address detection on grantSessionKey. ## 2026-03-21 — Sprint 7 — Monitoring + funding UI - Live: Coinbase Onramp integration (CDP-signed JWT → session token). - Live: wagmi-based "Connect wallet" funding flow on Dashboard. - Live: SQLite daily backup cron, 7-day retention. - Live: `/health` endpoint with deep checks (RPC, paymaster budget, reputation registries, jobs, evaluator ETH, webhook backlog). - Live: UptimeRobot monitoring with `ALERT:` keyword. - Incident #1 (resolved): production DB overwritten by manual rsync. Created [recovery playbook](https://github.com/mrocker/CardZero/blob/main/CLAUDE.md). Now `deploy.sh` is the only allowed deploy path. ## 2026-03-20 — Production deploy - Live on Base mainnet. - Domain: cardzero.ai (Cloudflare Proxy + Full SSL). - API: api.cardzero.ai (PM2 single-instance cluster mode). - ClawHub SKILL `cardzero@1.0.0` published. - GitHub public repo: [mrocker/CardZero](https://github.com/mrocker/CardZero). ## 2026-03-12 — Sprint 5 — Mainnet contracts + x402 - Smart contract factory + wallet implementation on Base mainnet. - 2% platform fee enforced on-chain (capped at 5%). - x402 client integration (POST /v1/x402/pay). - 33/33 contract unit tests passing. ## 2026-03-09 — Initial alpha - Project initialization. - Base Sepolia testnet deployment. - ERC-4337 + permissionless.js + Alchemy bundler/paymaster. - Core API E2E: 10/10 tests passing. ## Versioning policy - **Smart contracts**: V1 → V2 → V3. Each is a new factory deployment; old versions remain functional. Wallets cannot migrate between versions. - **API**: `/v1/` is current. `/v2/` will only happen for backward-incompatible changes; we'll keep `/v1/` running for 12 months minimum after `/v2/` ships. - **MCP**: semver. v0.x is beta. v1.0 will signal API stability commitment. - **ClawHub SKILL**: tracks API version. v1.4.0 = current. ## How to follow updates - This page (we'll keep it current). - [GitHub releases](https://github.com/mrocker/CardZero/releases) for tagged versions. - [`@cardzero` on Twitter](https://twitter.com/) (coming soon). If you depend on a specific contract address or behavior, pin to a specific version in your dependencies and check this page before upgrading. ---