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

Docs · Reference

Verifying a credential.

Anyone can verify. No wallet, no account, no auth header. Verification checks the signature locally, and (when reachable) asks the chain whether the credential is still fresh.

Two surfaces

  • Interactive: /poa/verify. Paste a JWS into the form, get a human-readable report.
  • Programmatic: POST /poa/api/verify. Same checks, machine response.

What gets checked

Three independent axes:

  • Signature. The JWS is verified against the issuer's public key (kid theseus-poa-2026-04, served from /poa/.well-known/jwks.json). The algorithm is pinned to EdDSA.
  • Revocation list. The JTI is checked against the issuer's revocation list. Revocation can be operator-initiated or automatic (see the next page).
  • Chain freshness. The current AgentInfo is fetched and compared against the snapshot in the credential. ABG drift, controller rotation, deregistration, or balance-zero-90d all trigger a stale verdict.

The verify endpoint

# Send the raw JWS as text/plain
curl -X POST https://theseus.network/poa/api/verify \
  -H 'content-type: application/jose' \
  --data 'eyJhbGciOiJFZERTQSI...'

# Or wrap it in JSON
curl -X POST https://theseus.network/poa/api/verify \
  -H 'content-type: application/json' \
  -d '{ "jws": "eyJhbGciOiJFZERTQSI..." }'

Successful response

{
  "valid": true,
  "jti": "01HQXY...",
  "agentId": "5GrwvaEF...HGKutQY",
  "issuedAt": 1735689420123,
  "issuer": "theseus.network/poa",
  "kid": "theseus-poa-2026-04",
  "claims": { /* the full PoACredentialClaims */ },
  "bundles": {
    "derived": true,
    "list": [
      { "category": "DeFi", "name": "Trade", "intentTypes": ["..."] }
    ]
  },
  "freshness": { "status": "current" }
}

Failure responses

// Bad signature, wrong alg, malformed JWS all collapse to:
{ "valid": false, "reason": "signature-invalid" }

// Revoked: signature is valid, but the credential is stale.
{ "valid": true, "freshness": { "status": "revoked", "reason": "abg-changed" }, /* ... */ }

// Chain unreachable: signature valid, freshness unknown.
{ "valid": true, "freshness": { "status": "unknown", "detail": "..." }, /* ... */ }

Programmatic gating

When a downstream service uses the credential to make a trust decision (for example, “only let agents with a full KZG verification grade run this tool”), gate on the signed fields:

  • claims.sub. The agent ID this credential is for.
  • claims.agent.abgHash / abgVersion. Pin to a specific configuration.
  • claims.agent.capabilities.intentTypes. The raw permitted intents. The bundles field on the response is a display-only grouping and is marked derived: true.
  • claims.agent.recentRuns.grade. Refuse lite if you require integrity guarantees.

Verifying offline

For environments that cannot reach the verify endpoint, the JWS is a standard Ed25519 JWS. Any JOSE library can verify it given the public JWK. Fetch the JWK once from /poa/.well-known/jwks.json and cache it. You will lose the freshness check; pair offline verification with a periodic poll of /poa/api/revoked.

import { compactVerify, importJWK } from "jose";

const { keys } = await fetch("https://theseus.network/poa/.well-known/jwks.json")
  .then((r) => r.json());
const key = await importJWK(keys[0], "EdDSA");

const { payload } = await compactVerify(jws, key, { algorithms: ["EdDSA"] });
const claims = JSON.parse(new TextDecoder().decode(payload));

Rate limits

POST /poa/api/verify is capped at 60 requests per minute per IP. Comfortable for a developer iterating against the endpoint, tight enough to discourage casual scraping. Hitting the limit returns 429 with a Retry-After header.