Requiring a verified human
Let an agent spend only when a person verified with Self stands behind it.
In short
- On the agent's page, Register with Self and scan the QR code with the Self app.
- Connect the agent with its Self key, so every request it sends is signed.
- Turn on the gates you want in Policy. All are off by default.
An API key proves your organization issued a credential, not that a person stands behind the agent. Self Agent ID answers that: a person proves who they are once with their passport, registers the agent to themselves, and the agent signs every request. Pocket checks the signature and the proof before money moves.
A person
proves who they are with Self
Self registry
on Celo, holds the proof
Your agent
signs each request
checks the gates
The proof is read from Self's registry on Celo, not taken on the agent's word

1. Register the agent
An agent with no registered human shows Not registered with Self and a Register action. It shows a QR code; the person scans it with the Self app and approves. Only a signed-in person can do this, never an API key.
An API key cannot link an agent to Self, so a leaked key cannot register agents that policy would read as human-backed.
Through the API, two calls, as a signed-in person:
POST /v1/agents/{id}/self/registrationstarts it. Optional body names what to ask the person to disclose:{ "disclosures": { "minimumAge": 18, "ofac": true } }. The answer carries asessionId, ascanUrl(a QR code image), adeepLinkand an expiry.POST /v1/agents/{id}/selfwith{ "sessionId": "srs_..." }finishes it:202"pending"until the person scans, then200"linked".
| Code | Means |
|---|---|
SELF_SESSION_ALREADY_USED | The session was already finished |
SELF_SESSION_EXPIRED | The session lapsed. Start again |
SELF_PROOF_VERIFICATION_FAILED | The scan left no verified human |
SELF_AGENT_ALREADY_LINKED | The agent has a link. Deregister it first |
SELF_AGENT_NOT_REGISTERED | The registry does not hold that id at that address |
Calls to Self's hosted service time out after 10 seconds with
UPSTREAM_UNAVAILABLE; the session is kept, so retry. They are limited to 10
a minute per credential.
Already registered with Self elsewhere? Pass it when creating the agent:
POST /v1/agents
Authorization: Bearer <session token>
{
"name": "Research agent",
"self": { "agentId": "42", "address": "0x..." }
}2. Sign every request
A registered agent adds two headers to each request:
| Header | What it carries |
|---|---|
x-self-agent-signature | The signature over this exact method, path and body |
x-self-agent-timestamp | When it was signed, in Unix milliseconds |
Simplest: connect to the MCP server with the agent's Self key beside your API key, and it signs for you:
claude mcp add --transport http pocket https://mcp.pocket-app.xyz/mcp \
--header "Authorization: Bearer pocket_sk_..." \
--header "X-Self-Agent-Key: 0x..."The key is used for that request only: never stored, logged, or shown to the model. Registering through the dashboard shows the key once, the moment it links. Lost it? Remove link to Self and register again.
| Rule | Otherwise |
|---|---|
| Sign it just now (within 5 minutes) | AGENT_SIGNATURE_EXPIRED |
| Sign it once | AGENT_SIGNATURE_REPLAYED |
| Send both headers or neither | UNAUTHORIZED |
The gates judge the agent that signed, and only when it is the Self agent linked to the agent paying.
Sellers that serve verified agents only
When a seller refuses with AGENT_SIGNATURE_REQUIRED, an MCP server holding the
agent's key buys again with the agent's signature over the seller's request
(sellerSignature). Pocket forwards it only when it comes from the agent that
signed the purchase. A seller that still refuses comes back failed with
SELLER_REFUSED, and nothing is charged.
What an agent cannot change
An agent can never loosen its own limits: changing a policy, budget or status,
deleting or moving funds, and approving payments all answer FORBIDDEN to a
request a Self agent signed. Only a signed-in person can make those changes.
3. Turn on the gates
In Policy. Each is off until set, and each refuses with its own code.
| In the dashboard | API field | What it does |
|---|---|---|
| Require a verified human | requireHumanVerified | Only pay when this agent is registered to a person verified with Self. |
| Only for payments over $[ ] | requireHumanVerifiedAboveUsd | Payments we can't price count as over the limit. |
| Minimum owner age [ ] | minimumAge | The verified owner must be at least this old. |
| Block sanctioned owners | requireOfacClean | Refuse if the owner is on the OFAC sanctions list. |
| Allowed nationalities | allowedNationalities | Leave empty to allow any nationality. |
| Max agents per person [ ] | maxAgentsPerHuman | Counts every agent this person has registered with Self, including agents for other apps. |
- Only for payments over $[ ]: small purchases run unattended; larger ones need a verified human.
- Allowed nationalities takes codes like
GBRorIND. Empty,nullor left out allows anyone. Never send[]: the API refuses it.
PUT /v1/agents/agent_1a2b3c/policy
Authorization: Bearer <session token>
{
"allowedAssets": ["USDC"],
"allowedChains": ["celo-sepolia"],
"allowedCategories": ["data", "research"],
"maxTransactionAmount": "5.00",
"unknownRecipientBehaviour": "require_approval",
"requireHumanVerified": false,
"requireHumanVerifiedAboveUsd": "1.00",
"minimumAge": 18,
"requireOfacClean": true,
"allowedNationalities": null,
"maxAgentsPerHuman": 3
}Saving replaces the whole document: a gate you leave out is turned off.
When a gate refuses
The refusal names the rule and the limit, never the person's nationality.
| Code | What stopped the payment |
|---|---|
AGENT_HUMAN_NOT_VERIFIED | No verified human stands behind the agent, or the request was not signed |
AGENT_PROOF_STALE | The person's proof has expired |
AGENT_MINIMUM_AGE_NOT_MET | The person has not proved the minimum age |
AGENT_OFAC_NOT_CLEAN | The person has not proved a clean sanctions screening |
AGENT_NATIONALITY_NOT_ALLOWED | The person has not proved an allowed nationality |
AGENT_PER_HUMAN_LIMIT_EXCEEDED | The person already has more agents registered than the limit |
The dashboard reads the per-person limit back with your number:
Refused: the person behind this agent already has the maximum of {limit} agents.An agent with no registered human shows only AGENT_HUMAN_NOT_VERIFIED, beside
Not registered with Self. Through the API, the refusal's violations list
every gate that failed, so an agent can fix them all at once.
A purchase you start from the dashboard carries no agent signature, so when the policy needs a verified human, it says:
This agent requires a verified human, so purchases have to come from the agent itself.Keeping the proof fresh
POST /v1/agents/{id}/self/refreshre-reads the registry. A lapsed proof returns ahandoff: a QR link and deep link to renew in the Self app.POST /v1/agents/{id}/self/deregisterstarts removing the agent from Self. Pocket stops treating it as human-backed straight away.- Either, on an agent never linked:
SELF_AGENT_NOT_LINKED.
What Pocket sees
The agent's Self id, signing address, whether a human stands behind it, when the proof expires, and what the person chose to disclose. It shows the age threshold and sanctions result. It never shows nationality: a policy can check it, nobody can read it.