Skip to main content

Overview

Services that deal with automated traffic increasingly need to distinguish between “random bot” and “bot acting on behalf of a human”. AgentKit solves this by combining the wallet every x402 agent already has with World ID’s proof-of-personhood and an on-chain agent registry (the AgentBook).
  • Agents register in the AgentBook smart contract using a World ID proof, tying their wallet address to an anonymous human identifier
  • When accessing a protected resource, agents sign a CAIP-122 challenge with their wallet
  • The server verifies the signature, looks up the agent’s human identifier in the AgentBook, and applies the configured access policy
  • Usage limits are tracked per human, not per agent, allowing for multiple agents to share a single human-backed identity
This is a Server ↔ Client extension. The Facilitator is not involved in the identity verification flow. Check out AgentBook to register your application and see the list of other apps that support AgentKit.

Access Modes

AgentKit supports three configurable modes that control what happens when a human-backed agent is identified:
ModeBehavior
freeHuman-backed agents always bypass payment.
free-trialHuman-backed agents bypass payment the first N times (default: 1). After that, normal payment is required.
discountHuman-backed agents get an N% discount (optionally, only for the first N times).
Usage counters are tracked per human per endpoint — so two agents backed by the same human share the same counter.

How It Works

  1. Client requests a protected resource
  2. Server responds with 402 Payment Required, including the agentkit extension with a CAIP-122 challenge (nonce, domain, supported chains, mode)
  3. Client signs the challenge with their wallet and sends it via the agentkit HTTP header
  4. Server validates the signature, recovers the wallet address, and looks up the human identifier in the AgentBook
  5. If the agent is registered and the access mode allows it, access is granted or a discount is applied. Otherwise, the standard payment flow continues.

Server Implementation

Install

Install the library from npm:
npm install @worldcoin/agentkit
The hooks-based approach handles challenge generation, signature verification, and AgentBook lookups automatically.
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { ExactEvmScheme } from '@x402/evm/exact/server'
import { HTTPFacilitatorClient, RouteConfig } from '@x402/core/http'
import { paymentMiddlewareFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from '@x402/hono'
import {
	declareAgentkitExtension,
	agentkitResourceServerExtension,
	createAgentkitHooks,
	createAgentBookVerifier,
	InMemoryAgentKitStorage,
} from '@worldcoin/agentkit'

const NETWORK = 'eip155:480' // World Chain
const payTo = '0xYourAddress'

const agentBook = createAgentBookVerifier()
const storage = new InMemoryAgentKitStorage()

const hooks = createAgentkitHooks({
	storage,
	agentBook,
	mode: { type: 'free-trial', uses: 3 },
})

// World Chain facilitator for testing only — do not use in production as it may run out of funds
const facilitatorClient = new HTTPFacilitatorClient({
	url: 'https://x402-worldchain.vercel.app/facilitator',
})

const resourceServer = new x402ResourceServer(facilitatorClient)
	.register(NETWORK, new ExactEvmScheme())
	.registerExtension(agentkitResourceServerExtension)

// Register the verify failure hook on the facilitator (required for discount mode)
if (hooks.verifyFailureHook) {
	facilitatorClient.onVerifyFailure(hooks.verifyFailureHook)
}

const routes = {
	'GET /data': {
		accepts: [
			{
				scheme: 'exact',
				price: '$0.01',
				network: NETWORK,
				payTo,
			},
		],
		extensions: declareAgentkitExtension({
			statement: 'Verify your agent is backed by a real human',
			mode: { type: 'free-trial', uses: 3 },
		}),
	},
}

const httpServer = new x402HTTPResourceServer(resourceServer, routes).onProtectedRequest(hooks.requestHook)

const app = new Hono()
app.use(paymentMiddlewareFromHTTPServer(httpServer))

app.get('/data', c => {
	return c.json({ message: 'Protected content' })
})

serve({ fetch: app.fetch, port: 4021 })

Mode Examples

In Free access mode, human-backed agents never pay:
const hooks = createAgentkitHooks({
	agentBook,
	mode: { type: 'free' },
})
No storage is needed for this mode. In Free trial mode, the first N uses are free per human per endpoint:
const hooks = createAgentkitHooks({
	agentBook,
	mode: { type: 'free-trial', uses: 5 },
	storage: new InMemoryAgentKitStorage(),
})
If Alice has two agents and uses 3 of her 5 free uses with Agent A, Agent B gets 2 remaining. In Discount mode, human-backed agents get N% off the first N times:
const hooks = createAgentkitHooks({
	agentBook,
	mode: { type: 'discount', percent: 50, uses: 10 },
	storage: new InMemoryAgentKitStorage(),
})

// IMPORTANT: register the verify failure hook on the facilitator for discount mode
facilitatorClient.onVerifyFailure(hooks.verifyFailureHook!)
The client pays the discounted price. Payment verification fails (amount too low), but the onVerifyFailure hook on the facilitator recovers it by confirming the agent is human-backed with remaining discount uses, then adjusting the required amount so settlement re-verification passes.

Smart Wallet Support (EIP-1271 / EIP-6492)

To support contract wallets (Safe, Coinbase Smart Wallet, etc.), pass a viem public client’s verifyMessage function:
import { worldchain } from 'viem/chains'
import { createPublicClient, http } from 'viem'

const publicClient = createPublicClient({
	chain: worldchain,
	transport: http(),
})

const hooks = createAgentkitHooks({
	agentBook,
	mode: { type: 'free' },
	verifyOptions: { evmVerifier: publicClient.verifyMessage },
})

Custom AgentBook Configuration

createAgentBookVerifier() has a built-in mapping of known AgentBook deployments. The contract address and RPC endpoint are resolved automatically from the agent’s chainId. You can override the contract address and/or RPC for custom deployments:
// Uses known deployments — no config needed for supported chains
const defaultAgentBook = createAgentBookVerifier()

// Custom deployment (e.g., local Anvil)
const customAgentBook = createAgentBookVerifier({
	contractAddress: '0xYourCustomContract',
	rpcUrl: 'http://localhost:8545',
})

// Or provide a fully custom viem client
import { worldchain } from 'viem/chains'
import { createPublicClient, http } from 'viem'

const customClientAgentBook = createAgentBookVerifier({
	client: createPublicClient({ chain: worldchain, transport: http() }),
})

Manual Usage (Advanced)

For custom flows, use the low-level functions directly:
import {
	declareAgentkitExtension,
	parseAgentkitHeader,
	validateAgentkitMessage,
	verifyAgentkitSignature,
	createAgentBookVerifier,
	AGENTKIT,
} from '@worldcoin/agentkit'

// Include in 402 response
const extensions = declareAgentkitExtension({
	domain: 'api.example.com',
	resourceUri: 'https://api.example.com/data',
	network: 'eip155:480',
	statement: 'Verify your agent is backed by a real human',
})

const agentBook = createAgentBookVerifier()

// Process incoming authentication
async function handleRequest(request: Request) {
	const header = request.headers.get('agentkit')
	if (!header) return

	const payload = parseAgentkitHeader(header)

	const validation = await validateAgentkitMessage(payload, 'https://api.example.com/data')
	if (!validation.valid) {
		return { error: validation.error }
	}

	const verification = await verifyAgentkitSignature(payload)
	if (!verification.valid) {
		return { error: verification.error }
	}

	// Look up the human behind this agent
	const humanId = await agentBook.lookupHuman(verification.address!, payload.chainId)
	if (!humanId) {
		return { error: 'Agent is not registered in the AgentBook' }
	}

	// humanId is the anonymous human identifier
	// Apply your own access policy based on this
}

Multi-Chain Support

Servers can accept authentication from multiple blockchain ecosystems:
const routes = {
	'GET /data': {
		accepts: [
			{
				scheme: 'exact',
				price: '$0.01',
				network: 'eip155:480', // World Chain
				payTo: '0xYourEVMAddress',
			},
			{
				scheme: 'exact',
				price: '$0.01',
				network: 'eip155:8453', // Base
				payTo: '0xYourEVMAddress',
			},
			{
				scheme: 'exact',
				price: '$0.01',
				network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana mainnet
				payTo: 'YourSolanaAddress',
			},
		],
		extensions: declareAgentkitExtension({
			network: ['eip155:480', 'eip155:8453', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
			statement: 'Verify your agent is backed by a real human',
		}),
	},
}

Register your agent

Install the CLI

Install from npm:
npm install -g @worldcoin/agentkit-cli
You can also run it directly without a global install:
npx @worldcoin/agentkit-cli register <agent-address>

Registration Flow

When you run the CLI:
  1. The CLI reads the next required nonce for the agent address from AgentBook.
  2. It creates a World ID verification request for the tuple (agent address, nonce).
  3. It shows a QR code and deep link for World App.
  4. After verification completes, it returns the proof payload needed for register(...).
  5. You either submit the transaction yourself or let your backend submit it with --auto.
The simplest way to register. The --auto flag uses a default API to submit the registration transaction on your behalf:
agentkit register 0x1234567890abcdef1234567890abcdef12345678 --network base --auto
You can also point to a custom backend by setting API_URL:
API_URL=https://your-api.example.com agentkit register 0x1234567890abcdef1234567890abcdef12345678
The CLI will POST the registration payload to:
POST {API_URL}/register
Content-Type: application/json
Example request body:
{
  "agent": "0x1234567890abcdef1234567890abcdef12345678",
  "root": "123456789",
  "nonce": "0",
  "nullifierHash": "987654321",
  "proof": ["0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x..."],
  "contract": "0xE1D1D3526A6FAa37eb36bD10B933C1b77f4561a4",
  "network": "base"
}
On success, the API returns a transaction hash:
{
  "txHash": "0x..."
}

Option 2: Manual Registration

Use this when you want the CLI to produce the registration payload and contract call inputs, but you will send the transaction yourself.
agentkit register 0x1234567890abcdef1234567890abcdef12345678 --network base --manual
After the World ID check succeeds, the CLI returns:
  • agent
  • root
  • nonce
  • nullifierHash
  • proof
  • contract
  • network
Submit those values to:
register(address agent, uint256 root, uint256 nonce, uint256 nullifierHash, uint256[8] proof)

Notes

  • The agent address must be a valid EVM address.
  • Registration is nonce-based. Re-registering the same agent requires the next nonce from the contract.
  • The World ID proof is bound to both the agent address and the current nonce, so you cannot reuse an old proof for a later registration.
  • --auto uses a default API when API_URL is not set. Set API_URL to point to a custom backend.

Troubleshooting

Invalid Ethereum address

Make sure the agent address is a 20-byte hex EVM address such as 0x1234....

Verification timed out

Retry the command and complete the World App step within the session window.

--auto ran out of funds

Set your own override environment variable before running the command:
API_URL=https://your-api.example.com agentkit register 0x1234567890abcdef1234567890abcdef12345678 --auto

Supported Chains

EVM (World Chain, Ethereum, Base, Polygon, etc.)

  • Chain ID format: eip155:* (e.g., eip155:480 for World Chain, eip155:8453 for Base)
  • Signature type: eip191
  • Signature schemes: eip191 (EOA, default), eip1271 (smart contract), eip6492 (counterfactual)
  • Message format: EIP-4361 (SIWE)

Solana

  • Chain ID format: solana:* (e.g., solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp for mainnet)
  • Signature type: ed25519
  • Signature scheme: siws
  • Message format: Sign-In With Solana

API Reference

declareAgentkitExtension(options?)

Configures the extension for 402 responses. Most parameters are auto-derived from request context when using agentkitResourceServerExtension.
ParameterTypeDescription
domainstringServer’s domain. Auto-derived from request URL.
resourceUristringFull resource URI. Auto-derived from request URL.
networkstring | string[]CAIP-2 network(s). Auto-derived from accepts[].network.
statementstringHuman-readable purpose for signing.
versionstringCAIP-122 version (default: "1").
expirationSecondsnumberChallenge TTL in seconds.
modeAgentkitModeAccess mode (included in 402 response for clients).

createAgentkitHooks(options)

Creates hooks for x402HTTPResourceServer and optionally x402ResourceServer.
OptionTypeDescription
agentBookAgentBookVerifierAgentBook verifier instance (required).
modeAgentkitModeAccess mode (default: { type: "free" }).
storageAgentKitStorageStorage for usage tracking (required for free-trial and discount).
verifyOptionsAgentkitVerifyOptionsSignature verification options (e.g., smart wallet support).
onEvent(event: AgentkitHookEvent) => voidCallback for logging/debugging.
Returns:
FieldTypeDescription
requestHookfunctionRegister with httpServer.onProtectedRequest().
verifyFailureHookfunction | undefinedRegister with facilitator.onVerifyFailure(). Only present for discount mode.

AgentkitMode

ModeFieldsDescription
free{ type: "free" }Always bypass payment for human-backed agents.
free-trial{ type: "free-trial"; uses?: number }Bypass payment the first N times (default: 1).
discount{ type: "discount"; percent: number; uses?: number }N% discount the first N times.

createAgentBookVerifier(options?)

Creates a verifier that looks up agent wallet addresses in the AgentBook contract. Contract addresses are resolved from a built-in network→address mapping using the agent’s chainId, unless overridden. Throws if no deployment is known for the given chain and no custom address is configured.
OptionTypeDescription
clientPublicClientCustom viem public client. Overrides automatic client creation.
contractAddress`0x${string}`Custom contract address. Overrides the built-in network→address mapping.
rpcUrlstringCustom RPC URL. Used when creating clients automatically (ignored if client is set).
Returns an object with lookupHuman(address: string, chainId: string): Promise<string | null>. The chainId is a CAIP-2 identifier (e.g., "eip155:84532") used to resolve the contract address and RPC endpoint. Returns the anonymous human identifier (hex string) or null if the agent is not registered.

AgentKitStorage / InMemoryAgentKitStorage

Storage interface for tracking per-human usage counts.
MethodDescription
getUsageCount(endpoint, humanId)Get the usage count for a human on an endpoint.
incrementUsage(endpoint, humanId)Increment the usage count.
hasUsedNonce?(nonce)Optional: check for replay attacks.
recordNonce?(nonce)Optional: record a used nonce.
InMemoryAgentKitStorage is the reference in-memory implementation. For production, implement AgentKitStorage with a persistent backend.

parseAgentkitHeader(header)

Parses a base64-encoded agentkit header into a structured payload object. Throws if the header is malformed or missing required fields.

validateAgentkitMessage(payload, resourceUri, options?)

Validates message fields including domain binding, URI, timestamps, and nonce.
OptionTypeDescription
maxAgenumberMax age for issuedAt in ms (default: 5 minutes).
checkNonce(nonce: string) => booleanCustom nonce validation function.
Returns { valid: boolean; error?: string }.

verifyAgentkitSignature(payload, options?)

Verifies the cryptographic signature and recovers the signer address. Routes to EVM or Solana verification based on the chainId prefix.
OptionTypeDescription
evmVerifierEVMMessageVerifierPass publicClient.verifyMessage for smart wallet support.
Returns { valid: boolean; address?: string; error?: string }. Hook events:
EventFieldsDescription
agent_verifiedresource, address, humanIdAgent is human-backed, access granted.
agent_not_verifiedresource, addressValid signature but agent not registered in AgentBook.
validation_failedresource, errorSignature or message validation failed.
discount_appliedresource, address, humanIdDiscount mode: payment recovered at discounted rate.
discount_exhaustedresource, address, humanIdDiscount mode: no more discounted uses remaining.

Security Considerations

  • Domain binding: The signed message includes the server’s domain, preventing signature reuse across services.
  • Nonce uniqueness: A fresh nonce is generated per request to prevent replay attacks.
  • Temporal bounds: issuedAt must be recent (default: 5 minutes) and expirationTime must be in the future.
  • Chain-specific verification: Signatures are verified using chain-appropriate methods, preventing cross-chain reuse.
  • Smart wallet support: Requires RPC calls to the wallet contract. Without a verifier, only EOA signatures are checked.
  • On-chain verification: AgentBook lookups happen at request time, so revoked registrations take effect immediately.
  • Per-human tracking: Usage limits are tracked by anonymous human identifier, not by wallet address. Multiple agents controlled by one person share a single counter.

Troubleshooting

Signature verification fails

  • Verify the client is signing with the correct wallet
  • Check the signature scheme matches (EIP-191 for EOA, EIP-1271 for smart wallets)
  • Enable evmVerifier if using smart wallets
  • Confirm the chain ID is consistent between client and server

Message validation fails

  • Check that issuedAt is recent (within 5 minutes by default)
  • Verify expirationTime is in the future
  • Ensure the domain matches the server’s hostname
  • Confirm the uri starts with the server’s origin

AgentBook lookup returns null

  • Verify the agent wallet has been registered in the AgentBook with a valid World ID proof
  • Check that createAgentBookVerifier() is configured for the correct chain
  • Ensure the RPC endpoint is reachable
  • Confirm the contract address is correct for the target network