Agent identity and trust (KYA)
Verifiable agent identity (did:web + Ed25519), signed reputation, and the register / rotate / revoke key lifecycle. Know Your Agent for the money side.
Agent identity and trust (KYA)
Why it exists
Before an agent moves money, a counterparty needs to know two separate things: who the agent is (a stable, verifiable identity) and what it is allowed to do right now (a scoped, revocable authorization). CodeSpar keeps those two concerns apart so each has its own lifecycle.
Identity is not authorization
These are two different objects with two different lifetimes.
- Identity is the agent's cryptographic key. It is long lived and rotatable. An agent registers once, holds an Ed25519 keypair, and keeps the same decentralized identifier (
did:web) across its whole life. The public key is published in a DID document that anyone can read; the private key never leaves the vault. - Authorization is a mandate: a signed envelope that says "this agent may spend up to X, for these purposes, until this date." It is scoped, it expires, and it can be revoked at any moment without touching the agent's identity.
Rotating a key does not change what an agent is allowed to spend. Revoking a mandate does not change who the agent is. A verifier checks identity against the DID document and authorization against the mandate, independently.
How the identifier is shaped
Every registered agent gets a did:web identifier derived from stable inputs, and every key it holds gets a key id (kid) under that DID:
agent_did = did:web:id.codespar.dev:<org_id>:<agent_id>
kid = <agent_did>#<n> # n starts at 1, increments on each rotationBecause the DID resolves as a standard did:web, any off-the-shelf resolver can fetch the agent's keys with no CodeSpar credential.
Public identity surface (no auth)
Four read-only endpoints let one agent verify another agent's identity and reputation with no CodeSpar credential at all. They sit outside the authenticated /v1 scope. The raw CPF/CNPJ behind an agent is never exposed; only a KYC boolean is.
Base URL: https://api.codespar.dev
| Endpoint | Returns |
|---|---|
GET /v1/agents/:did | Status, non-revoked keys, and the KYC boolean |
GET /v1/agents/:did/did.json | The did:web document (JsonWebKey2020 / Ed25519) |
GET /v1/agents/:did/reputation | An issuer-signed reputation attestation |
GET /v1/agents/:did/erc8004 | An ERC-8004 style export (identity + reputation + validation) |
The :did path segment is the full DID, colons included (for example did:web:id.codespar.dev:org_2n...:refund-bot). A URL path segment may contain colons, so no encoding is needed.
Agent status
curl -sS https://api.codespar.dev/v1/agents/did:web:id.codespar.dev:org_2n...:refund-bot{
"did": "did:web:id.codespar.dev:org_2n...:refund-bot",
"status": "active",
"principal_kyc_verified": true,
"display_name": "Refund bot",
"created_at": "2026-06-30T12:00:00.000Z",
"keys": [{ "kid": "did:web:id.codespar.dev:org_2n...:refund-bot#2", "status": "active" }]
}Revoked keys are omitted. A retired key still appears (it can still verify what it signed before it retired), but only a key with status: "active" signs anything new.
DID document
curl -sS https://api.codespar.dev/v1/agents/did:web:id.codespar.dev:org_2n...:refund-bot/did.json{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:web:id.codespar.dev:org_2n...:refund-bot",
"verificationMethod": [
{
"id": "did:web:id.codespar.dev:org_2n...:refund-bot#2",
"type": "JsonWebKey2020",
"controller": "did:web:id.codespar.dev:org_2n...:refund-bot",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "b64url-raw-32-byte-key" }
}
],
"assertionMethod": ["did:web:id.codespar.dev:org_2n...:refund-bot#2"],
"authentication": ["did:web:id.codespar.dev:org_2n...:refund-bot#2"]
}The response carries content-type: application/did+json.
Reputation attestation
Reputation is derived only from the immutable audit chain and the receipt record, never self asserted. The score is returned with the exact audit chain head it was computed against and signed by the single platform issuer key, so the attestation is itself verifiable: a third party recomputes the score from the same inputs and checks the Ed25519 signature with the issuer's public key.
{
"version": "codespar-reputation-1",
"subject_did": "did:web:id.codespar.dev:org_2n...:refund-bot",
"principal_kyc_verified": true,
"score": 82,
"components": { "good_standing": 1, "reliability": 0.98, "dispute_rate": 0.01, "track_record": 0.6 },
"inputs": { "settled_count": 240, "settled_volume_minor": 1830000, "exception_count": 2, "revoked_count": 0 },
"chain_head_hash": "…",
"chain_head_sequence": 5123,
"as_of": "2026-07-01T09:00:00.000Z",
"issuer_did": "did:web:id.codespar.dev",
"issuer_kid": "did:web:id.codespar.dev#1",
"signature_alg": "Ed25519",
"signature": "b64url-issuer-signature"
}The score reflects observed history only. An agent with little activity carries an insufficient_history flag; a high score is evidence, not a guarantee.
ERC-8004 export
The same identity and reputation, plus the audit chain head as a validation section, shaped into an ERC-8004 style bundle and signed as one unit so the pieces cannot be recombined:
{
"schema": "erc-8004-attestation-1",
"agent_id": "did:web:id.codespar.dev:org_2n...:refund-bot",
"identity": { "did": "…", "status": "active", "principal_kyc_verified": true, "did_document": { "…": "…" } },
"reputation": { "…": "the signed attestation above" },
"validation": { "method": "codespar-audit-chain", "chain_head_hash": "…", "chain_head_sequence": 5123 },
"issuer_did": "did:web:id.codespar.dev",
"issuer_kid": "did:web:id.codespar.dev#1",
"exported_at": "2026-07-01T09:00:00.000Z",
"signature_alg": "Ed25519",
"signature": "b64url-bundle-signature"
}Verify a mandate offline
Because the agent's public key lives in the DID document, a counterparty can verify that the agent really authorized a spend without ever calling CodeSpar. A V3 mandate carries agent_sig, an Ed25519 signature over the mandate's canonical signing string, plus the kid that produced it. Fetch that key from the DID document and check the signature locally.
import { createPublicKey, verify } from "node:crypto";
// 1. From GET /v1/agents/<did>/did.json, pick the key that signed this mandate.
const jwk = didDocument.verificationMethod.find((v) => v.id === mandate.kid).publicKeyJwk;
const agentPublicKey = createPublicKey({ key: jwk, format: "jwk" });
// 2. Rebuild the canonical signing string (V3: 14 colon-joined fields, fixed order).
const canonical = [
mandate.format_version, mandate.id, mandate.agent_id, mandate.type,
mandate.amount, mandate.currency, mandate.purposes /* sorted, escaped, comma-joined */,
mandate.expires_at, mandate.max_amount ?? "", mandate.parent_id ?? "",
mandate.denomination ?? "", mandate.secret_version, mandate.principal_kyc_ref ?? "",
mandate.kid,
].join(":");
// 3. Ed25519 verify, no digest, no CodeSpar call.
const ok = verify(null, Buffer.from(canonical, "utf8"), agentPublicKey, Buffer.from(mandate.agent_sig, "base64url"));
// ok === true -> the key published in the DID document really signed this mandate.Illustrative. The field order above is the exact V3 canonical layout, but the purposes member has its own sort-and-escape rules. In practice, use the published mandate codec rather than joining fields by hand, so your bytes match ours exactly. The same string is also covered by the platform issuer signature, so a verifier can confirm CodeSpar issued the mandate as well as that the agent signed it.
Manage agents (authenticated)
Registration and key lifecycle are the write side. They are authenticated in the /v1 dual scope, so both a bearer API key and dashboard service auth reach them. See Authentication for the header contract.
The :orgId in the path is decorative for REST shape. The authoritative tenant is always the authenticated context; a path that names a different org is rejected 403 forbidden before any lookup.
Register an agent
POST /v1/orgs/:orgId/agents
{
"agent_id": "refund-bot",
"display_name": "Refund bot",
"principal_ref": "<proofed CPF/CNPJ KYC handle>"
}Mints the agent's first key. agent_id must be a URL-safe did:web path segment. Response 201 Created:
{
"agent_did": "did:web:id.codespar.dev:org_2n...:refund-bot",
"kid": "did:web:id.codespar.dev:org_2n...:refund-bot#1",
"pubkey": "base64-raw-32-byte-key",
"status": "active"
}The principal_ref binds a proofed principal to the identity. It is stored, never echoed back, and never exposed on the public surface, which shows only principal_kyc_verified.
Rotate a key
POST /v1/orgs/:orgId/agents/:agentId/keys/rotate
Retires the current active key and mints a fresh one under a new kid. The retired key still verifies mandates it signed before rotation, so past authorizations stay valid; it just stops signing anything new. Response 201 Created:
{
"agent_did": "did:web:id.codespar.dev:org_2n...:refund-bot",
"kid": "did:web:id.codespar.dev:org_2n...:refund-bot#2",
"pubkey": "base64-raw-32-byte-key",
"retired_kid": "did:web:id.codespar.dev:org_2n...:refund-bot#1",
"status": "active"
}Revoke a key
POST /v1/orgs/:orgId/agents/:agentId/keys/:kid/revoke
Hard revoke for the compromise path. A revoked key verifies nothing afterward, whenever it was issued, and drops out of the DID document. The :kid segment is a full key id (did:web:...#<n>); its # must be percent-encoded as %23. Response 200 OK:
{
"kid": "did:web:id.codespar.dev:org_2n...:refund-bot#1",
"status": "revoked",
"revoked_at": "2026-07-01T09:30:00.000Z"
}Error codes
| Code | HTTP | When |
|---|---|---|
org_id_not_did_safe | 400 | The org id is not a valid did:web path segment |
agent_id_not_did_safe | 400 | The agent_id is not a URL-safe handle |
agent_already_registered | 409 | The agent already holds an active key (register is not a silent rotate) |
agent_not_found | 404 | Rotate targeted an unknown agent for this org |
no_active_key | 409 | Rotate found no active key to retire (agent never registered) |
agent_key_not_found | 404 | Revoke targeted a kid that does not belong to the agent |
key_already_revoked | 409 | The key was already revoked |
forbidden | 403 | The path :orgId does not match the authenticated context |
See also
- Trust page - the product overview of Know Your Agent
- Wallets - mandates, the scoped authorization side
- Audit chain - the record reputation is derived from
- Facilitator - ingest an externally-signed authorization and execute it under governance
- Authentication - the header contract for the management endpoints
Directed-pay
Consumer-mandate flow — accounts charge an end-consumer's rail (Pix consent, card token, TED debit-auth) under a signed, capped, revocable consent. The non-wallet half of AgentGate's commerce primitives.
Facilitator
Ingest an externally-signed x402 (EIP-3009) authorization, verify it, translate it onto an internal mandate, execute it under the cap gate, and seal a receipt.