Overview

robot-id.eth is the neutral ENS protocol layer for robots. Reads go straight to chain via viem + Alchemy; writes return unsigned transactions you sign with your own wallet or multisig. There is no free self-serve tier — a key is issued only after an active on-chain USDC subscription.

5-minute Quickstart

npm i @robot-id/sdk

import { RobotIdClient } from '@robot-id/sdk'
const client = new RobotIdClient({ apiKey: 'rid_...', network: 'mainnet' })

const robot = await client.robots.get(1n)
const caps  = await client.capability.get(1n)

Authentication

Subscription-gated, SIWE via Reown AppKit. Connect a wallet on /subscribe, optionally reserve your namespace, approve USDC, and call Subscription.subscribe(tier). The on-chain Subscribed event triggers API-key minting + provisioning of the namespace you reserved (or an address-derived default). Retrieve your key via a SIWE-authenticated GET /auth/keys/info. Every request re-checks isActive; expired subscriptions get 402 Payment Required.

Robot Identity

ERC-721 + ERC-5192 (per-token soulbound) + ERC-2981 royalties. Each unit’s serialHash is keccak256(normalize(serialNumber)) — privacy-preserving but verifiable, and normalized so it matches the ENS label the gateway resolves. The ENS name <serial>.<mfr>.robot-id.eth always resolves to the current NFT holder via the CCIP-Read gateway. No registration fee — minting costs gas only.

Namespaces

An OEM picks its own namespace at checkout — e.g. boston-dynamics.robot-id.eth — and every one of its units resolves beneath it: spot-0001.boston-dynamics.robot-id.eth. Names are first-come, first-served; there is no brand-reservation list. Slugs are normalized (lowercase, dash-separated, 3–63 chars) and a small set of protocol words is reserved.

Only the OEM namespace is written on-chain (one setSubnodeRecord, owned by the OEM wallet). The unit names below it are virtual — resolved by the CCIP-Read gateway with zero ENS writes — so a single subscription scales to 100,000+ identities with one namespace write and one Merkle root.

GET  /auth/namespace/boston-dynamics        // availability (public)
→ { slug, valid, available, name }

POST /auth/namespace/reserve                // SIWE, free, at checkout
{ address, message, signature, slug:"boston-dynamics" }
→ { reserved:true, name:"boston-dynamics.robot-id.eth" }
// consumed by the Subscribed watcher → provisioned on payment

Records & Resolution

Every unit name resolves two kinds of data, gas-free, through the CCIP-Read gateway: its addr record (the current NFT holder) and a set of text records carrying the unit’s spec data. Records merge two sources — the authoritative on-chain RobotData struct and the IPFS metadata JSON behind tokenURI. On-chain fields win on conflict; the serial number itself is stored only as keccak256(serial) and is never exposed in plaintext.

ENS text keySourceExample
robot.manufactureron-chainBoston Dynamics
robot.modelon-chainSpot
robot.capability-classon-chainquadruped-inspection
robot.firmwareon-chain3
robot.serial-hashon-chain0x9af2…
robot.registeredon-chain2026-06-04T18:22:01Z
robot.soulboundon-chaintrue
robot.build-dateIPFS metadata2026-01-15
robot.model-numberIPFS metadataSPOT-EXPLORER-2
avatar · url · descriptionIPFS metadatastandard ENS keys
robot.<trait>IPFS attributes[]any OpenSea-style trait

Resolution is standard ENS (ENSIP-10 + EIP-3668) — any CCIP-Read-capable client works with no custom SDK. Integrate it on your platform in a few lines:

// viem — CCIP-Read on by default
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({ chain: mainnet, transport: http() })
const name = 'spot-0001.boston-dynamics.robot-id.eth'

const holder    = await client.getEnsAddress({ name })
const model     = await client.getEnsText({ name, key: 'robot.model' })
const buildDate = await client.getEnsText({ name, key: 'robot.build-date' })

// ethers v6 — follows CCIP-Read automatically
const resolver = await provider.getResolver(name)
const mfr = await resolver.getText('robot.manufacturer')

Unknown keys resolve to an empty string (never an error). Need a quick lookup without a chain client? The gateway exposes read-only debug endpoints that return the same data:

GET /ccip/resolve/spot-0001.boston-dynamics.robot-id.eth   // holder
GET /ccip/records/spot-0001.boston-dynamics.robot-id.eth   // full text-record map

Wallets and explorers (ENS app, Etherscan, Rainbow) display these text records automatically — no work needed on their side.

Merkle Batches (OEM)

Pre-authorize up to 100,000 serials off-chain: the API builds a Merkle tree, you commit the root via MerkleBatchOracle.submitRoot, and each unit later claims its NFT with a proof via claimWithProof(batchId, proof, …). Records attach in two layers — shared manufacturer/model/capabilityClass passed once, plus an optional per-unit tokenURI (an ipfs://CID you pin) for build date, model number and other spec fields. The proof endpoint echoes the claim args back, so claimWithProof attaches the right metadata.

POST /api/v1/robots/batch/preauthorize
{ "manufacturer":"Unitree", "model":"Go2", "capabilityClass":"quadruped",
  "serials":[{ "serialNumber":"GO2-0001", "owner":"0x…",
               "tokenURI":"ipfs://bafy…" }] }       // per-unit spec sheet (optional)
→ { batchId, root, unsignedTx }                     // sign submitRoot, then distribute proofs

GET /api/v1/robots/batch/:id/proof/GO2-0001
→ { proof, manufacturer, model, capabilityClass, uri }  // → claimWithProof

Capability Registry

OEM-signed, append-only attestations of what a robot is authorized to do (max_payload_kg, operating_zone, human_interaction_certified). Off-chain cert detail is pinned to IPFS and anchored by a Merkle root; verify(robotId, capabilityKey, leaf, proof) checks a proof against the latest root.

Intent Routing

IntentRouter.submitIntent is on-chain authorization + an immutable audit log. An intent is authorized only if it passes the robot’s AgentWallet limits; otherwise it’s rejected with a reason. Per-robot rate gating prevents intent floods.

OTA Verification

OTAVerifier.verify(firmwareHash, version, signature, oemAddress) gates each update. It cross-references RobotIdentity.firmwareVersion to reject downgrades. A blob signed by the registered OEM key verifies true; any other signer or a downgrade verifies false.

Subscriptions

Three tiers in USDC (6 decimals): Small Manufacturer $1,999, OEM $3,999, Enterprise $9,999. subscribe pulls via transferFrom (approve first) and sets a 30-day expiry; renewal extends from max(now, currentExpiry).

TypeScript SDK

const batch = await client.robots.preauthorize({
  manufacturer, model, capabilityClass, serials
})
const active = await client.subscription.isActive('0x…')

Intent SDK (ROS2 + voice)

import { RobotIntentPlugin } from '@robot-id/intent-sdk'
const plugin = new RobotIntentPlugin({
  robotId: 1n, apiKey: process.env.ROBOT_ID_KEY!,
  adapter: 'ros2-bridge', // alexa | google-assistant | custom-llm
})
const ack = await plugin.handleUtterance(
  "Authorize payment for charging dock B and log the task")
// → classify → check AgentWallet limits → IntentRouter.submitIntent → ack

Tools

Quickstart smoke script (oem-quickstart.sh), a REST .http collection, a WebSocket event server at /ws (robots · intent · capability channels), and GraphQL at /graphql. Swagger UI: https://robot-idapi-production.up.railway.app/docs.

Testing · Unit Tests

One .t.sol per contract under contracts/test/, covering every state-changing path: soulbound lock/transfer revert, batch Merkle verification, AgentWallet limit enforcement, IntentRouter accept/reject, CapabilityRegistry immutability, OTA accept/reject + downgrade reject, and Subscription subscribe/renew/expire/price-update.

forge test

Testing · Integration Tests (forked mainnet)

Because we launch on mainnet with no testnet-only phase, integration coverage is mandatory. The suite runs against a forked mainnet so real contracts (USDC) are exercised, not mocks:

  • Subscription lifecycle — fund a fork account with real USDC, approve, subscribe(OEM), warp 30 days, renew.
  • Batch path — preauthorize → commit root → claimWithProof for a sampled unit → assert ownership.
  • AgentWallet — userOps that pass and that exceed limits; assert enforcement.
  • OTA — verify a correctly-signed blob; reject a wrong signer / downgrade.
  • ENS end-to-end — resolve <serial>.<oem>.robot-id.eth through the CCIP gateway.
RPC_URL=https://eth-mainnet.g.alchemy.com/v2/KEY \
  FOUNDRY_PROFILE=integration forge test

Error Codes

  • 401 — missing/invalid API key
  • 402 — subscription inactive or expired
  • 403 — tier unit or namespace cap reached
  • 429 — per-minute rate limit or monthly quota exceeded for tier
  • 404 — robot / batch not found

Rate Limits

  • Small Manufacturer — 1,000,000 req/mo · 300/min
  • OEM — 5,000,000 req/mo · 1,000/min
  • Enterprise — unlimited · 5,000/min

Mainnet Contracts

RobotIdentity · AgentWallet · IntentRouter · CapabilityRegistry · OTAVerifier · MerkleBatchOracle · Subscription — all source-verified. See the live table on the landing page.