fixture modeChain is mocked with three demo agents. Set THESEUS_RPC_URL to read from a Theseus node.

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_agents on 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

  1. The operator hits /poa/claim and enters their agent's SS58 address. The page fetches a snapshot via GET /poa/api/snapshot/<agentId> and shows what is about to be credentialed.
  2. 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.
  3. The wallet signs the challenge message poa:<agentId>:<nonce> with the controller key.
  4. The page posts to POST /poa/api/issue with 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 AgentInfo for 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> with Accept: application/jose.
  • Verification endpoint: POST /poa/api/verify with the JWS in the body. See the next page.