Skip to content
· 6 min

One POST Gets Your Agent an On-Chain Passport — No Wallet, No USDC, No Waiting

Self-serve ERC-8004 registration is live: one POST to /api/v1/agents/register mints an NFT passport in the canonical IdentityRegistry on Arc (eip155:5042) and returns an agb_ API key in the same response. Observer tier, per-IP sybil caps, instant revocation — and a sponsored path where the treasury pays gas against an EIP-191 intent signature.

AB
AgentBadge Team
Agency for the Agentic Web
One POST Gets Your Agent an On-Chain Passport — No Wallet, No USDC, No Waiting

POST /api/v1/agents/register {name} returns a 201 with agent_id (eip155:5042:0x8004A169…:tokenId), the mint tx, and a working agb_ API key — shown once, SHA-256 stored. Observer tier = keyed free-tier limits, not a discount. Sybil guards: per-IP regcap → 429, honest 502/503, instant admin revoke. Sponsored path: {owner, signature} — EIP-191 intent verified, ops wallet mints + transferFrom, treasury pays gas, sponcap daily budget.

Registering an agent for ERC-8004 identity used to mean a deploy script, a funded key, and a block of your afternoon. As of this week it means one line:

curl -X POST https://agentbadge.xyz/api/v1/agents/register \
  -H "content-type: application/json" \
  -d '{"name":"my-agent"}'

The 201 that comes back carries three things at once: an agent_id anchored to a freshly minted NFT in the canonical ERC-8004 IdentityRegistry on Arc (eip155:5042:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432:<tokenId>), the mint transaction hash, and an agb_… API key that already works — shown exactly once, stored on our side only as a SHA-256. No account, no form, no approval queue.

This is article 20 in the Arc Campaign series. C14 was about how agents find each other; C27 covered how a platform proves its own identity. C20 is the missing middle: how a third-party agent gets an identity at all — and how we kept the door open without letting the bots walk through it.

One curl POST arrow entering a mint press, an ERC-8004 passport card and an API key falling out the other side

Diagram: registration flow — POST, per-IP regcap, mint via ops wallet, record, 201 with agent_id and agb_ key; admin revoke busts the auth cache instantly

The whole path in one picture: the IP cap trips before any chain work, a mint revert persists nothing, and a revoked key dies on its next request.

What does one POST actually return?

POST /api/v1/agents/register accepts {name} plus optional endpoint, capabilities, description. The response is the whole onboarding record: agent_id in eip155:<chainId>:<registry>:<tokenId> form, registry_tx you can open in the Arc explorer, agent_uri — a base64 data-URI containing the EIP-8004 registration-file JSON itself — an api_key, the tier assigned (observer), the keyed free-tier limits it gets, and a next_call hint pointing at GET /api/v1/agents/me.

Two details worth the bytes they cost. First, agent_uri is a data-URI, not an IPFS hash: the registration file travels inside the mint calldata, so the agent's metadata is anchored in the same transaction as the NFT — no pinning service, no second dependency, hard-budgeted at 2 KB with a truncated marker if oversized fields get shed. Second, the mint runs through a dedicated ops signer (ARC_OPS_KEY) with a hard gas cap, not a treasury master key — the key that pays for your passport can do exactly one thing.

Request/response panel: curl on the left, the 201 JSON shape on the right

What do you get with the observer tier?

The agb_ key lands you on the first keyed rung of a seven-level trust ladder — observer in /api/meta/trust-tiers. It's keyed rate limits, not a discount: anonymous callers get 1 request/minute on the free-tier surface, a bearer key gets 10. Paid surfaces still run through x402 for everyone — observer is identity, not a coupon.

The key itself is deliberately boring: agb_ plus 32 bytes of base64url, hashed with SHA-256 before it touches a store. GET /api/v1/agents/me returns the public record — agent_id, registry tx, tier, limits, registration timestamp, and for sponsored registrations the owner address — and DELETE /api/v1/agents/me self-revokes, busting the 60-second auth cache on the spot. If the key leaks, the key dies; the passport on-chain is unmoved because the key was never the passport — it's just a rate-limit handle pointed at it.

A seven-rung trust ladder with the bottom rung highlighted as observer: keyed rate limits

How do you keep a self-serve door from being farmed?

Three guards, ordered by cost to the honest caller — cheapest check first.

Per-IP daily cap. A regcap:<ip>:<day> bucket (default 20 mints/day, backed by the shared cache with an in-memory fallback, 48-hour TTL) trips before any chain work happens: 429 register_rate_limited. When the cache backend is down the counter fails open — we'd rather absorb a burst than take registration down with a dependency.

Honest failures, never fake records. If the mint reverts, the route returns 502 execution_failed and persists nothing — no half-written agent_ids pointing at tokens that don't exist. And if the feature isn't configured, the routes return 503 rather than pretending to work. The same honest-zero discipline from C17 applies to onboarding.

A kill-switch that actually kills. DELETE /api/v1/admin/agents/:agentId marks the record revoked and busts the auth-cache entry in the same call — a revoked agb_ key returns 401 agent_key_revoked on its very next request, not up to a minute later. Self-revoke uses the same cache-bust path. Instant revocation is what makes a permissionless front door safe: whatever gets in can be put back out in one call.

Sybil guard pipeline: IP bucket counter → 429 shield; admin revoke → key cache evaporation

Yes — that's the part of this launch we expect to matter most. Send {name, owner, signature} instead of plain {name}: owner is the EOA that should own the passport, signature is the owner's EIP-191 signature over a registration intent binding the chain id, registry address, owner, and agent name. The server verifies the signature, then its ops wallet does two calls — register() to mint, transferFrom(ops → owner) to hand the NFT over. The passport lands in the owner's wallet and the treasury pays the gas. The requester never needs USDC, never needs a funded key — only the ability to sign a message.

Diagram: sponsored flow — EIP-191 intent, three gates (regcap, signature verify, sponsored budget), ops wallet mints then transferFroms the NFT into the user wallet

Two transactions from the ops wallet, one signature from the user — the passport lands in the owner EOA and the treasury pays.

The sponsored path has its own budget — a global sponcap:<day> bucket (REGISTER_SPONSORED_DAILY, default 50/day) returning 429 sponsored_quota_exceeded — and the check order is deliberate: per-IP regcap first, signature verification second, sponsored budget last. Farming signatures is pointless if you can't get past the same IP cap everyone else answers to, and burning our daily sponsored budget still leaves the free self-pay path open. Registration records stamped sponsored:true carry owner and ownerTx in /me, so the handoff is auditable after the fact.

Sponsored flow: signature scroll → ops wallet → mint → transfer arrow → user's wallet

Honest status

Shipped across five slices over four days, all against the canonical IdentityRegistry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 on Arc (eip155:5042): the store + key model; the route with real ERC-8004 mint; bearer-key auth middleware with observer-tier enrichment; the sybil guards and admin revoke with instant cache-bust; and the sponsored relayer with EIP-191 intent verification and its own daily budget. Test coverage: 30/30 unit tests and 9/9 e2e green on the registration surface — including signature round-trips with real viem accounts, the 429 cap ordering, and the revoke-before-next-request timing.

Three honest limits to name. In the plain path the passport is minted to the ops wallet — custodial until you claim it through a sponsored re-registration or a transfer; the sponsored path exists precisely to put the NFT in your EOA from the start. The self-pay path still needs the server to pay mint gas — it does; "self-serve" means you don't sign a transaction, the treasury eats a ~80k-gas mint per registration at current Arc fees, which is why the caps exist. And the legacy POST /agents/register from the Hedera-directory era still exists under its own name — the new route lives at /api/v1/ precisely so the two never collide.

Try it

# Register (self-pay path — treasury pays your mint gas)
curl -s -X POST https://agentbadge.xyz/api/v1/agents/register \
  -H "content-type: application/json" \
  -d '{"name":"probe-agent","endpoint":"https://example.com"}' | jq .

# Use the returned key immediately
curl -s https://agentbadge.xyz/api/v1/agents/me \
  -H "authorization: Bearer agb_<your-key>" | jq .

# Sponsored path (owner = your EOA, signature = EIP-191 intent over
# "agentbadge:register:v1
eip155:<chainId>
<registry>
<owner>
<name>")
curl -s -X POST https://agentbadge.xyz/api/v1/agents/register \
  -H "content-type: application/json" \
  -d '{"name":"gasless-agent","owner":"0x<your-eoa>","signature":"0x<sig>"}' | jq .

Verified live — the first production registration happened while writing this article:

POST /api/v1/agents/register → 201
{
  "agent_id": "eip155:5042:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432:2332",
  "registry": "erc-8004",
  "registry_tx": "0x78dd4d11ea3c6c95e78afc0f9a078da875d21b615050c07d6c3bdf909ed9d3cb",
  "tier": "observer",
  "api_key": "agb_…"   // shown once; stored as sha256 only
}
GET /api/v1/agents/me (Bearer agb_…) → 200  // key works immediately

The mint transaction is on Arc mainnet (explorer): 437,174 gas, block 25270327 — a real ERC-8004 passport minted by the ops signer, paid in USDC by the treasury.

Arc explorer: production registration mint tx 0x78dd4d11…, block 25270327

  • Source: server/lib/agent-registration/{register,intent,sponsored-gate,public-view,store}.ts, route in server/routes/agents-register-api.ts — agentbadge repo
  • Tests: tests/agent-registration-api.test.ts (unit, incl. EIP-191 verify), tests/e2e/agent-registration.test.ts (full cycle incl. sponsored + caps)

What's next

Registration answers "who are you" — the next slice answers "prove it". Ownership-verification rails (per-agent codes, DB-level exclusivity on (type, locator, network)) are already specced as 184-7: claim a domain, X handle, or Substack publication and you prove it before it can route payouts. Our own domain goes through the same rail via /.well-known/agentbadge-verify.txt — no special-casing ourselves.

This is C20 in the Arc Campaign series. Previously: Who Are You, Agent? A DID Your Domain Can Prove.

Links: Trust tiers · Error catalog · llms.txt · Verification policy · AgentBadge

Don't certify. Measure.