Docs · Reference
Issuing a credential.
Operators mint by signing a one-time nonce with the agent's controller key. No passwords, no API keys. The chain's controller is the only authorization.
Prerequisites
- The agent must be registered in
pallet_agentson the Theseus chain the deployment is pointed at. - The operator must hold the agent's controller key in a browser wallet (Polkadot.js, Talisman, SubWallet, anything that exposes sr25519 signing through the Polkadot extension API).
- The agent's seus balance must be non-zero. A deregistered or empty agent cannot be credentialed.
The flow, end to end
- The operator hits
/poa/claimand enters their agent's SS58 address. The page fetches a snapshot viaGET /poa/api/snapshot/<agentId>and shows what is about to be credentialed. - The page requests a challenge via
POST /poa/api/challenge. The server returns a 16-byte hex nonce bound to the agent ID, valid for five minutes. - The wallet signs the challenge message
poa:<agentId>:<nonce>with the controller key. - The page posts to
POST /poa/api/issuewith the agent ID, nonce, and signature. The server consumes the challenge atomically, verifies the signature against the on-chain controller, mints the JWS, and returns the credential URLs.
Step 1. Request a challenge
curl -X POST https://theseus.network/poa/api/challenge \
-H 'content-type: application/json' \
-d '{ "agentId": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" }'{
"nonce": "9b4f...e21c",
"agentId": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"message": "poa:5GrwvaEF...HGKutQY:9b4f...e21c",
"expiresAt": 1735689600000
}message is what the wallet must sign. It is scoped by the poa: prefix so a controller signature for PoA cannot be replayed against another protocol that prompts for a signed message.
Sign in the browser
import { web3Enable, web3FromAddress } from "@polkadot/extension-dapp";
await web3Enable("Proof of Agenthood");
const injector = await web3FromAddress(controllerAddress);
const { signature } = await injector.signer.signRaw({
address: controllerAddress,
data: message, // from the challenge response
type: "bytes",
});
// signature is a 0x-prefixed hex string; strip the 0x for the issue request.Step 2. Issue
curl -X POST https://theseus.network/poa/api/issue \
-H 'content-type: application/json' \
-d '{
"agentId": "5GrwvaEF...HGKutQY",
"controllerSig": {
"nonce": "9b4f...e21c",
"signatureHex": "a3b1...f7"
}
}'{
"jti": "01HQXY...",
"agentId": "5GrwvaEF...HGKutQY",
"issuedAt": 1735689420123,
"credentialUrl": "/poa/api/credential/01HQXY...",
"pageUrl": "/poa/5GrwvaEF...HGKutQY"
}Fetch the JWS itself with GET /poa/api/credential/<jti>. Send Accept: application/jose for the raw JWS, or accept the default JSON wrapper.
Failure modes
- 400 challenge-expired-or-unknown. The nonce was already consumed or older than five minutes. Request a fresh one.
- 400 challenge-agent-mismatch. The nonce was issued for a different agent ID than the one in the issue request.
- 400 controllerSig-malformed. Signature is not a well-formed hex string of the expected length.
- 400 signature-invalid. The chain reader confirmed the signature does not verify against the agent's on-chain controller. Most often this means the wrong wallet account signed.
- 400 agent-not-registered. No
AgentInfofor the given SS58 address. - 503 chain-unreachable. The Theseus RPC endpoint is down or misconfigured. PoA fails loud rather than silently falling back to fixtures.
- 429. Five issues per five-minute window per IP. The response includes
Retry-After.
Where the credential lives
After a successful issue, the credential is reachable in three places:
- Public page:
/poa/<agentId>. The human-readable credential surface. - Raw JWS:
/poa/api/credential/<jti>withAccept: application/jose. - Verification endpoint:
POST /poa/api/verifywith the JWS in the body. See the next page.