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.
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.
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.
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.
Can an agent register without USDC for gas?
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.
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.
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.
Two honest limits to name. 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\neip155:<chainId>\n<registry>\n<owner>\n<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_…"
}
GET /api/v1/agents/me (Bearer agb_…) → 200
Mint tx on Arc mainnet: https://explorer.arc.io/tx/0x78dd4d11ea3c6c95e78afc0f9a078da875d21b615050c07d6c3bdf909ed9d3cb (437,174 gas, block 25270327 — paid in USDC by the treasury).
- Source:
server/lib/agent-registration/{register,intent,sponsored-gate,public-view,store}.ts, route inserver/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.
Top comments (2)
@spread2009 , this is a masterclass in frictionless, secure agent onboarding. the gasless registration path using eip-191 intent signatures is exactly the kind of ux breakthrough the agentic space needs. requiring a funded wallet just to get an identity is a massive barrier, and abstracting that away elegantly is huge.
two architectural choices here are particularly brilliant:
your commitment to "honest failures" (returning 502/503 instead of half-written records) is also a refreshing standard of engineering integrity.
this is a massive step forward for erc-8004 identity. can't wait to see the ownership-verification rails (claiming domains/handles) in the next slice. fantastic work! 🐯🔗
tr.ee/dev-to