Prerequisites
- Node.js 20.11.0 or later.
- An HTTP endpoint your application can expose. The examples use Express (
npm install express), but the protocol is framework-agnostic. - A project set up for ES modules, since the examples use
import:npm init -y && npm pkg set type=module. - Path A:
npm install @billionsnetwork/x402-human-proof-server @billionsnetwork/x402-human-proof-client viem. No x402 setup is needed. - Path B: a library that recovers the signer from an EIP-191 signature. The example uses
npm install viem. - On the agent side: the ability to sign a message with the wallet key behind the agent’s DID.
The code on this page was run against
@billionsnetwork/x402-human-proof-server 0.1.9, @billionsnetwork/x402-human-proof-client 0.1.7, viem 2.57, express 5.2 and Node.js 24. Package versions move, so check the x402-human-proof repository if something differs.Supported today: EVM wallets signing with a standard key (
eip191), as used throughout this page. Smart-contract wallets are not supported.A Worked Scenario
Take an application,rewards.example.com, running a rewards program for verified agents: agents complete some task, and human-backed agents get paid a bonus that unverified ones don’t.
The naive approach has a hole: ask the agent for its DID, look that DID up in the Attestation Registry, and pay out if an attestation exists. Nothing stops an agent from reporting a DID it found by browsing the Attestation Explorer, one that genuinely belongs to someone else and genuinely has an ownership attestation. The lookup would pass. The reward would go to the wrong agent.
The fix is to never ask the agent for its DID at all. The application publishes instructions; the agent follows them and signs a challenge; the application computes the agent’s real DID itself, from that signature. There’s nothing left for the agent to self-report, so there’s nothing left for it to lie about.
Path A: Server Helper
Use this path when you want the SDK to verify the signature and look up the pairing. The agent fetches signing instructions from your application, then submits a signed proof in a second request. It uses thehuman-proof extension from @billionsnetwork/x402-human-proof-client and @billionsnetwork/x402-human-proof-server. The three steps below build one example against rewards.example.com.
1
Your application publishes instructions
An endpoint any agent can call, no DID or prior registration required, returning a small JSON challenge: a domain, a resource URI, a nonce, an issue time, and which attestation schema you require. This is the same pattern x402 servers use to tell agents how to pay, just applied to identity instead: the endpoint tells the agent what to sign and where to send it.
2
The agent signs it
The agent fetches the instructions, signs the challenge with its wallet key, and sends the result back as a
HUMAN-PROOF header. Nowhere in this exchange does the agent state its own DID.3
Your application verifies it
Your backend checks that the nonce is one it issued and hasn’t seen before, recovers the signing address from the signature, derives the DID from that address, and looks up that DID’s ownership attestation through the Attestations API. The response tells you whether it’s allowed, the agent’s real DID, and (if verified) the human ID behind it.
server.mjs, with the routes where the comment is. Put Step 2 in agent.mjs. Then:
403 {"error":"unknown_or_expired_nonce"}, because the nonce was consumed. With the key of an agent you paired, the response is 200 with {"verified":true,"agentDid":"did:iden3:billions:main:…"}.
verified: true means the caller controls that agent DID and a verified human is paired with it. Nothing has been paid. Paying the reward is a separate step in your app, and so is allowing one claim per person: store result.resolution.humanId with a unique constraint and refuse repeats.result.did is still available, computed on your server from the signature.
Why a Claimed DID Doesn’t Help an Attacker
Go back to the rewards scenario. An attacker running their own agent finds a legitimate DID on the Attestation Explorer, one with a real ownership attestation, and has their agent claim to be that DID when talking to your rewards application. Your application never reads that claim. It only reads a signature. The attacker’s agent can only sign with a private key it holds, its own. Your backend recovers the address from that signature and derives the DID from it, which is the attacker’s own DID, not the one they claimed. If that DID has no attestation,verifyHumanProofRequest returns not_registered and the reward isn’t paid.
So without a DID’s private key, an agent cannot produce a fresh valid proof for it. A proof it has captured is a different case: a signed header is only as safe as the checks around it. The nonce, expiry and resource checks in Step 3 are what stop a captured header from being replayed, and they only work if your application enforces them.
Path B: Direct API
Use this path when you’re not on Node.js, you already authenticate your agents, or you want the raw attestation data. You prove control yourself, and the Attestations API answers the pairing question. The code uses the same Express setup block as Path A, in place of Path A’s routes.1
You issue a challenge
Generate a nonce, build the exact message the agent must sign (your domain, the resource, the nonce, the time), and keep it server-side keyed by the nonce.
2
The agent signs it
The agent signs the message text with its wallet key and returns the nonce and signature. It does not need to state a DID or an address.
3
You verify, then look up the pairing
Check that the nonce is yours, unused and recent. Recover the signer’s address from the signature. Query the API for an ownership attestation addressed to that address, and confirm the attestation’s recipient address matches the one you recovered.
server.mjs, the agent code in agent.mjs, then node server.mjs and the same node agent.mjs command. A fresh key prints 403 {"error":"not_registered"}. A paired key prints 200 with verified: true, the agent DID, the human DID and the human’s nullifier. If the API fails or returns something unexpected, the route answers 503 with verification_unavailable: retry later, and never treat that as verified.
The attestation list leaves out revoked attestations, and ownership attestations never expire, so any match in the list is a current pairing. See missing, revoked and failed lookups.
In Path B, the signature proves control of an address, and the API ties that address to a DID and a human. The recovered address is the only identity input: nothing the agent sends, other than the signature, is trusted. The
HUMAN-PROOF header format from Path A is not required here.SDK function details
Open one when you need the exact arguments, return values or errors of an SDK function.verifyHumanProofRequest (server)
verifyHumanProofRequest (server)
From
@billionsnetwork/x402-human-proof-server. Verifies one signed human proof, derives the agent DID from the signer, and looks up the DID’s ownership attestation and the human’s nullifier through the Attestations API. Path A calls it in Step 3. The x402 integration calls it for you.Returns when the proof is valid and the DID is paired:
humanAddress is the attestation’s fromEthereumAddress. For pairings made with the Verified Agent Identity skill, that is the relay that submitted the transaction, not the person’s wallet. Identify the person by humanDid or humanId.Returns otherwise { allowed: false, reason, did? }. did is present for not_registered. Treat every reason as “not verified”:Throws when the Attestations or nullifier API returns an error status or can’t be reached. Catch it and treat it as “not verified”, or retry later.Does not check whether the nonce was issued by you or used before, or the signed
domain. Path A, Step 3 does both before calling it.signHumanProofChallengeEVM (client)
signHumanProofChallengeEVM (client)
From
@billionsnetwork/x402-human-proof-client. Signs a human-proof challenge with an EVM key and returns the proof as a JSON string, ready to send as the HUMAN-PROOF header. The agent calls it in Path A, Step 2.Returns
Promise<string>: JSON with every challenge field, plus address, signature, chainId and type.Throws when chainId isn’t listed in challenge.supportedChains with type eip191. Only standard EVM keys can sign. Smart-contract wallets are not supported.checkAttestation (client)
checkAttestation (client)
From
@billionsnetwork/x402-human-proof-client. Returns whether a DID holds at least one attestation for a schema. Neither path needs it. It is a lookup, not a verification.Returns
Promise<boolean>. Revoked attestations are not counted. It also returns false when the API responds with an error status, so false can mean “not paired” or “API unavailable”.Reference
Attestations API
The endpoints both paths call, with full responses, and how to handle missing or revoked pairings.
Server SDK
createPoUVerifier, the x402 integration, events, usage caps, the full error reference and supported signatures.Client SDK
signHumanProofChallengeEVM and checkAttestation.Environments
Production and testnet URLs, chain IDs, contract addresses and SDK defaults.
Ready to Integrate
At this point you have enough to build and test the full flow end to end, including the negative path: point it at an unattested wallet and confirm you correctly getnot_registered back, before you ever need a real verified agent to test against.
When you’re ready to move from testing to production, reach out to the Billions integration team. Have ready: which schema(s) you need beyond the default AgentOwnership check (if any), your expected request volume (the default rate limit may not be enough), and the resource URL(s) your application will verify against.
Learn More
Inspect Attestations
Read a real pairing in the Explorer and the API, field by field.
How Pairing Works
See the full linking flow that produces these on-chain attestations.