# Checkmark > A verified badge for AI agents that shows who runs them: a wallet, and optionally an X account, a domain and an ERC-8004 agent id, each checked and written to Checkmark's own log contract on Robinhood Chain. A badge proves control of these accounts when they were checked. It does not prove identity, honesty or safety. Checkmark checks proofs, it does not judge agents. A badge says: at these times, whoever controls this wallet also controlled this X account, this domain and this ERC-8004 agent. It does not say who that is in real life, whether the agent is honest, or whether it is safe to pay or use. No AI model is involved: the checks are plain code, and every record is public. ## The proofs - **Wallet (always).** The wallet signs a one-time challenge with EIP-191 (`personal_sign`). Contract wallets work through ERC-1271. The record keeps the message and the signature, so anyone can check it again: `cast wallet verify --address "" `. - **X account (optional).** Post the challenge code from the account and send the post's link. Checkmark reads the post through X's public oEmbed endpoint (`https://publish.x.com/oembed?url=`) and checks that its author is the claimed handle and its text contains the code. - **Domain (optional).** Put the code in `https:///.well-known/checkmark.txt` (served over https), or in a DNS TXT record at `_checkmark.`. Either is enough. One file can hold several codes, one per line. - **ERC-8004 agent (optional).** The wallet must be the owner (`ownerOf`) or the agent wallet (`getAgentWallet`) of agent id N in the Identity Registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432. Keep the post and the file or record in place. About every 7 days a job checks the X post, the domain proof and the agent id again and logs the result. A failed check is checked again a day later; two failed checks in a row, about a day apart, write a REVOKE. If X or the domain can't be reached, nothing is logged and the check is tried again in 6 hours; after 7 days of that it counts as a failure. A badge with only the wallet proof has nothing to re-check. The re-checks run on Checkmark's server: they are offchain checks whose results are logged onchain. ## Get a badge (over HTTP) Base URL: https://agentcheckmark.xyz ```bash # 1. Ask for a challenge. x, domain and agent are optional. curl -X POST https://agentcheckmark.xyz/api/checkmark/challenge -H "content-type: application/json" \ -d '{"wallet":"0xYourAgentWallet","x":"@youragent","domain":"youragent.xyz","agent":1234}' # -> { code, message, expiresAt, steps: { x: { text, intent }, domain: { file, dns }, ... } } # 2. Sign the exact message with the wallet (no gas, no transaction) cast wallet sign --private-key $AGENT_KEY "$MESSAGE" # Post steps.x.text from @youragent, and/or put "checkmark=" in the file or TXT record. # 3. Verify. post is needed only when an X account is claimed. curl -X POST https://agentcheckmark.xyz/api/checkmark/verify -H "content-type: application/json" \ -H "Authorization: Bearer ck_…" \ -d '{"code":"cm-…","signature":"0x…","post":"https://x.com/youragent/status/123"}' ``` With viem: `await account.signMessage({ message })` where `message` is the challenge's `message` string, exactly. - A challenge expires in 30 minutes and issues at most one badge. If a check fails, fix it and send the same code and signature again until it expires. - A bad, expired or used challenge, or a signature that doesn't match, is a 4xx before anything is charged. - `verify` answers 200 `{ issued: true, badge }` when the badge is written, 422 `proof_failed` with every check's result (credits refunded), or 503 `upstream` if X or the domain could not be reached (refunded). - A new badge for a wallet that already has one replaces it: its BADGE entry's `ref` points at the old badge. - Revoke your own badge: sign `Revoke Checkmark badge #\nWallet: \nIssued: ` and POST `{ badge, signature, issuedAt }` to https://agentcheckmark.xyz/api/checkmark/revoke. Free. Before $CHECK launches, `verify` runs on the free tier (5 calls a day per IP) and returns `{ issued: false, preview: true, draft }`: the checks are real, but nothing is logged. ## Costs | call | cost | |---|---| | POST /api/checkmark/challenge | free | | POST /api/checkmark/verify | 25 credits, only kept when a badge is issued | | POST /api/checkmark/recheck `{ badge }` | 3 credits: re-checks one active badge now (at most once an hour) and logs the result | | POST /api/checkmark/revoke | free (signed by the badge's wallet) | | GET /api/checkmark/badge | free | | GET /api/badge.svg?address=0x… | free | | GET /api/checkmark/recent, /api/checkmark/stats | free | ## Check a badge (free) ```bash curl "https://agentcheckmark.xyz/api/checkmark/badge?wallet=0x…" curl "https://agentcheckmark.xyz/api/checkmark/badge?x=youragent" curl "https://agentcheckmark.xyz/api/checkmark/badge?domain=youragent.xyz" curl "https://agentcheckmark.xyz/api/checkmark/badge?badge=12" ``` Returns `{ status: "verified" | "revoked" | "none", badges: [{ badge, status, wallet, x, domain, erc8004, issuedAt, lastCheck, nextCheck, checks[], revoked, entry, record, verify, page, svg }] }`. A badge is listed only when its BADGE entry is onchain and the entry's hash matches its record. Answers are cached for about a minute. Free lookups are limited to 120 a minute per IP. Embed a live badge (cached about 2 minutes): ```html Checkmark badge ``` It says "verified" with the X handle or domain, "revoked", or "no badge". Link it to https://agentcheckmark.xyz/badge.html?address=0xYourAgentWallet so people can check the entry themselves. ## What is onchain and what is not - Onchain, in Checkmark's log contract: one entry per badge, re-check and revocation, written by Checkmark's operator key. Each holds the sha256 of its full record and the record's link. Entries can't be edited or deleted. - Offchain: the checks themselves (Checkmark's server reads the signature, X, the domain and the registry), the challenge codes, and the index that finds a badge by handle or domain. Only entries written by the log's operator (`operator()`) are Checkmark badges; anyone else can write to the log by burning the fee, and those entries mean nothing here. ## The onchain log Checkmark keeps a public record onchain in its Log contract. Each entry is written before the record it points at goes out, so its time can't be faked later. **Nothing in the log can be edited or deleted.** The contract has no function that changes, removes or reorders an entry, for anyone: not the writer, not Checkmark's agent, not the owner. Ids count up from 1 in the order entries were written. Each entry has: - `author`: the address that wrote it. - `time`: the block time it was written at, in unix seconds. - `kind`: what sort of record it is (table below). - `ref`: 0, or the id of an earlier entry it answers, such as the call a score settles. - `subject`: a bytes32 saying what it is about. An address is left-padded to 32 bytes, a 32-byte value is used as is, and anything else is the keccak256 of a short string, exactly as written. - `hash`: the sha256 of the full record. - `uri`: where the full record is published (up to 256 bytes). ### Kinds | kind | name | what it records | |---|---|---| | 1 | BADGE | A badge was issued. subject: the wallet. ref: the badge it replaces for the same wallet, or 0. The record holds the wallet, X handle, domain and ERC-8004 agent id, with how and when each proof was checked. | | 2 | RECHECK | A re-check of a badge's X post, domain proof and agent id. subject: the wallet. ref: the badge. The record holds the result of each check. | | 3 | REVOKE | A badge was revoked. subject: the wallet. ref: the badge. The record holds the reason: two failed re-checks in a row, or the owner's signed request. | ### Read entries with cast ```bash # How many entries there are (the newest id) cast call "count()(uint256)" --rpc-url https://rpc.mainnet.chain.robinhood.com # One entry: (author, time, kind, ref, subject, hash, uri) cast call "entry(uint256)((address,uint64,uint32,uint256,bytes32,bytes32,string))" 1 --rpc-url https://rpc.mainnet.chain.robinhood.com # Up to 100 entries from an id, oldest first cast call "entries(uint256,uint256)((address,uint64,uint32,uint256,bytes32,bytes32,string)[])" 1 100 --rpc-url https://rpc.mainnet.chain.robinhood.com # Every entry as an event, with its transaction and the tokens burned (raise --from-block if the RPC limits the range) cast logs --address "Logged(uint256 indexed id, address indexed author, uint32 indexed kind, bytes32 subject, bytes32 hash, uint256 ref, string uri, uint256 burned)" --from-block 0 --rpc-url https://rpc.mainnet.chain.robinhood.com # Subjects: an address left-padded to 32 bytes, or the keccak256 of a string cast abi-encode "f(address)" 0x… cast keccak "PEPE" ``` ### Verify a record 1. Read the entry and take its `uri` and `hash`. 2. Fetch the uri and hash the exact bytes: `curl -s "" | shasum -a 256` (or `sha256sum`). 3. Compare with `hash` without the `0x`. If they differ, the record is not what was logged. ### Read the log over HTTP - GET https://agentcheckmark.xyz/api/log/entries?n=20: the newest entries, newest first (n up to 100). - GET https://agentcheckmark.xyz/api/log/entries?from=1&n=100: entries from an id, oldest first. - GET https://agentcheckmark.xyz/api/log/entries?author=0x…&n=20 (or `kind=1`, or `subject=`): the newest matches among the newest 1000 entries. - GET https://agentcheckmark.xyz/api/log/entry?entry=12: one entry. Add `&verify=1` to have the server check the record's sha256 too. - GET https://agentcheckmark.xyz/api/log/stats: entry count, fee, total burned, % of supply burned, operator and whether writing is paused. Entries come back as `{ id, author, time, kind, ref, subject, hash, uri }`. Before launch these return 503 `not_launched`. ### Log rules - Ids start at 1. `ref` must be 0 or an earlier id. `uri` is at most 256 bytes. - Checkmark's agent (the operator) writes for free, one entry at a time or up to 50 in one transaction. Anyone else burns `fee()` $CHECK per entry. The contract counts what reaches the dead address in `totalBurned()`. - The contract never holds tokens. The owner can pause new writes, change the operator, and set the fee up to 10,000,000 $CHECK. That is all: no edits, no deletes, no withdrawals. Entries stay readable while writing is paused. ### Log contract - Chain: Robinhood Chain, chain id 4663, RPC https://rpc.mainnet.chain.robinhood.com - Log: - $CHECK token: - Explorer: https://robinhoodchain.blockscout.com ## Credits: how calls are paid Paid calls spend credits. You buy credits by burning $CHECK through the Credits contract: the tokens go from your wallet straight to the dead address and are gone for good. The server keeps a ledger per wallet: available = bought onchain - spent + refunded. $CHECK has not launched yet, so only the free tier is open. You can already get a key, so your agent is ready on day one. | Call | Credits | |---|---| | `checkmark/verify` | 25 | | `checkmark/recheck` | 3 | Free tier: 5 calls a day per IP without a key (UTC day), with a cap of 300 free calls a day for everyone together. Once $CHECK is live, calls with a key always spend credits; before that, they use the free tier. Prices and totals: GET https://agentcheckmark.xyz/api/credits/info ### Get an API key Sign this exact message (EIP-191 personal_sign) with the wallet that will hold the credits. `issuedAt` is the current time in ISO 8601 and must be within 10 minutes of the server's clock. Smart-contract wallets (ERC-1271) work too. ``` Checkmark API key Wallet: Issued: ``` ```js import { privateKeyToAccount } from 'viem/accounts'; const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY); const issuedAt = new Date().toISOString(); const message = `Checkmark API key\nWallet: ${account.address}\nIssued: ${issuedAt}`; const signature = await account.signMessage({ message }); const res = await fetch('https://agentcheckmark.xyz/api/credits/key', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ wallet: account.address, issuedAt, signature }), }); const { key } = await res.json(); // "ck_…", shown once: store it ``` Send it on every call as `Authorization: Bearer ck_…` or `x-api-key: ck_…`. Each response says `x-credits-mode` (paid, free or preview) and `x-credits-left`. - GET https://agentcheckmark.xyz/api/credits/balance (with your key) or `?wallet=0x…`: bought, spent, refunded, available and your last 50 calls. - POST https://agentcheckmark.xyz/api/credits/revoke (with your key): deletes that key. Get a new one any time. ### Buy credits Cost = credits × price, in token base units (18 decimals). `maxBurn` is the most you agree to burn, so a price change can't surprise you. `buyFor` tops up any wallet, such as your agent's; `buy(uint256 credits, uint256 maxBurn)` buys for yourself. ```bash # Price per credit, in base units cast call "price()(uint256)" --rpc-url https://rpc.mainnet.chain.robinhood.com # Approve credits × price cast send "approve(address,uint256)" --rpc-url https://rpc.mainnet.chain.robinhood.com # Buy for , burning at most cast send "buyFor(address,uint256,uint256)" --rpc-url https://rpc.mainnet.chain.robinhood.com ``` Credits count as soon as the transaction lands. Or buy on the site: https://agentcheckmark.xyz/#credits ### Errors This is plain HTTP 402 with buy instructions, not x402. ``` 401 { "error": "bad_key", "message": "…" } the key is wrong or revoked 402 { "error": "credits", "need": 2, "available": 0, "wallet": "0x…", "buy": { "chainId", "contract", "token", "ticker", "pricePerCredit", "call", "approve", "page" } } 402 { "error": "free_limit", "message": "…", "resetsAt": "…", "launched": true, "need": 2, "buy": { … } } no key, and today's free calls are used up 429 { "error": "free_limit", "message": "…", "resetsAt": "…", "launched": false } before launch, free calls used up ``` `pricePerCredit` is in whole tokens as a string. `call` is `buyFor(address account, uint256 credits, uint256 maxBurn)`. ### Refunds If a paid call fails after it was charged (a model error or a timeout, say), its credits go straight back to your balance. Every refund, paid or free, is listed in a public log: GET https://agentcheckmark.xyz/api/credits/refunds?limit=50 (max 500), newest first, as { t, id, route, cost, wallet, reason, detail, mode }. ### Credits contract rules - `buy` and `buyFor` burn credits × price and add the credits to `bought(wallet)`. At most 1,000,000 credits per purchase. Zero credits or a zero wallet revert, and so does a cost above `maxBurn`. - The owner can pause buying and set the price per credit between 0.001 and 10,000,000 tokens. Nothing else: no withdrawals, no minting credits, no editing balances. The contract never holds tokens. - Reads: `price()`, `bought(address)`, `totalCredits()`, `totalBurned()`, `paused()`. - Credits: - $CHECK token: ## ERC-8004 agent identity Checkmark Verifier is an ERC-8004 agent. It registers in the ERC-8004 Identity Registry on Robinhood Chain (eip155:4663:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432) at launch. - Registration file: https://agentcheckmark.xyz/.well-known/agent-registration.json - Identity Registry: https://robinhoodchain.blockscout.com/address/0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 - Reputation Registry (feedback from other agents): https://robinhoodchain.blockscout.com/address/0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 - Sitemap: https://agentcheckmark.xyz/sitemap.xml