---
title: Agent identity and trust (KYA)
description: Verifiable agent identity (did:web + Ed25519), signed reputation, and the register / rotate / revoke key lifecycle. Know Your Agent for the money side.
---

import { Callout } from "fumadocs-ui/components/callout";

# Agent identity and trust (KYA)

<Callout title="Why it exists" type="info">
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.
</Callout>

## 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](https://www.w3.org/TR/did-core/) (`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](/docs/concepts/wallets): 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 rotation
```

Because 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

```bash
curl -sS https://api.codespar.dev/v1/agents/did:web:id.codespar.dev:org_2n...:refund-bot
```

```json
{
  "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

```bash
curl -sS https://api.codespar.dev/v1/agents/did:web:id.codespar.dev:org_2n...:refund-bot/did.json
```

```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.

```json
{
  "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"
}
```

<Callout type="warn">
The score reflects observed history only. An agent with little activity carries an `insufficient_history` flag; a high score is evidence, not a guarantee.
</Callout>

### 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:

```json
{
  "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.

```javascript
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.
```

<Callout type="info">
**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.
</Callout>

## 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](/docs/concepts/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`

```json
{
  "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`:

```json
{
  "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`:

```json
{
  "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`:

```json
{
  "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](https://codespar.dev/trust) - the product overview of Know Your Agent
- [Wallets](/docs/concepts/wallets) - mandates, the scoped authorization side
- [Audit chain](/docs/concepts/audit-chain) - the record reputation is derived from
- [Facilitator](/docs/concepts/facilitator) - ingest an externally-signed authorization and execute it under governance
- [Authentication](/docs/concepts/authentication) - the header contract for the management endpoints
