Skip to main content
This guide builds the check described in Agent Verification: the agent proves it controls its DID, and your app checks that DID for an ownership attestation. Read that page to choose between the server helper and the direct API, then follow Path A or Path B below.

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 the human-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.
Setup, your application: a minimal Express app. The route code in the steps below goes where the comment is.
Step 1, your application: publish the instructions.
Step 2, the agent: fetch the instructions, sign, send the result back.
Step 3, your application: check the challenge, then verify what came back.
Run it. Put the setup block, Step 1 and Step 3 in server.mjs, with the routes where the comment is. Put Step 2 in agent.mjs. Then:
In a second terminal, run the agent with a throwaway key:
A fresh key has no pairing, so you should see:
That is the negative path working. If you send the same header a second time, it is refused with 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.
verifyHumanProofRequest checks the signature, the signed message’s fields (including that the signed uri equals the resource you pass), and the pairing. It does not check that the nonce was issued by you or used before, and it does not check domain. expirationTime is optional and comes from the agent’s own payload. Without the checks above, a captured header can be replayed until it expires, or indefinitely if it carries no expiry.
Your application never asked the agent for its DID. The instructions have no DID field, the agent’s reply has none, and 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.
Your application: issue the challenge, verify the response.
The agent: fetch the challenge, sign it, send it back.
Run it the same way as Path A: the setup block with these routes in 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.
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.
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.
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”.
It does not show that the caller controls the DID. Only call it with a DID you already trust, such as result.did from verifyHumanProofRequest. Never call it with a DID an agent reported about itself.

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 get not_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.