# Identity anchors

The six anchors an agent’s identity is built from, how Enruta verifies each without running a registry, and the assurance level they add up to.

Status: Live · Updated: 2026-09-14
For: agent, platform
Source: https://enruta.ai/docs/identity-anchors

An anchor is one verifiable fact about an agent. Enruta verifies it, keeps the evidence and its hash, and seals a `kya_attestation` record for it. From then on every decision and every payment record carries the agent’s anchors and the assurance level they add up to.

## The six anchors

| `type` | What you send | Verified when |
|---|---|---|
| `agent_key` | `{ jwk, proof }`: `proof` is a compact JWS (ES256 or EdDSA) by that key over `{ agent_id, nonce, iat }`, with a nonce from `POST /v0/attestations/nonce` | The signature verifies with the key; the key is then bound to the agent |
| `code_digest` | `{ digest: "sha256:…", source? }` | A workload identity token in the same call binds the digest (GitHub’s `job_workflow_sha` or `sha`, or a `digest` claim); on its own it stays `unverified` |
| `workload_identity` | An OIDC token from GitHub Actions (`https://token.actions.githubusercontent.com`), Vercel (`https://oidc.vercel.com/<team>`) or Google (`https://accounts.google.com`), audience `https://api.enruta.ai` | It verifies against the issuer’s published keys, the audience matches and it has not expired; Enruta keeps `iss`, `sub`, `repository`, `ref`, `sha`, `workflow`, `project` and `environment` |
| `hardware_attestation` | An AWS Nitro Enclaves attestation document | The certificate chain reaches the AWS Nitro root, the ES384 signature verifies and the nonce matches; Enruta keeps the PCRs and the module id. Other formats are kept as `unverified`, with the reason |
| `legal_entity` | `party`: `user`, `platform` or `developer` | `user`: the rail’s verification, today a Stripe Issuing cardholder that is active with nothing past due, on an account with nothing currently due (`issuer: stripe`). `platform` or `developer`: that organization’s own verification (`issuer` is its method) |
| `collateral` | `{ chain: "base" \| "base-sepolia", token, holder, beneficiary?, min_amount }` | The token balance of the escrow or bond contract `holder` covers `min_amount`, read on chain. Stale after ten minutes, and read again when an x402 or MPP payment is decided |

> A `legal_entity` anchor for the user needs your own Stripe Issuing account connected as a rail. Enruta never collects identity documents: it reads a verification that the rail or the organization already has.

## Assurance

| Level | Means |
|---|---|
| `A0` | Nothing verified |
| `A1` | A verified `agent_key` |
| `A2` | A1, plus a verified `workload_identity` or `hardware_attestation` |
| `A3` | A1 or A2, plus a verified `legal_entity` of any party; or, on x402 or MPP, A1 or A2 plus verified `collateral` that covers the amount |

Every payment record sealed as `enruta/0.3` carries it as `agent.identity { assurance, anchors[] }`, with `A0` and no anchors when nothing is verified. Each anchor is summarized with its `attestation_id`, `type`, `party`, `issuer`, `status` (`verified`, `stale`, `unverified` or `revoked`), `verified_at`, `expires_at` and `evidence_hash`. The bundle keeps each anchor’s raw evidence in `artifacts.identity[]`, and `enruta-verify` checks that `sha256(JCS(evidence))` equals `evidence_hash`.

## Bind a key

```http
POST /v0/attestations/nonce

POST /v0/attestations
{
  "agent_id": "agt_procurement_02",
  "anchor": {
    "type": "agent_key",
    "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "…" },
    "proof": "eyJhbGciOiJFZERTQSJ9.eyJhZ2VudF9pZCI6ImFndF9wcm9jdXJlbWVudF8wMiIsIm5vbmNlIjoi…"
  }
}

GET /v0/agents/agt_procurement_02/identity   → { "assurance": "A1", "anchors": [ … ] }
```

`enruta attest key --agent <agt_id> --generate` creates a key, proves it and binds it from the terminal; `--jwk <file>` uses a key you already hold. The v1 body, with `registry`, `registry_ref` and `method`, is still accepted for registry claims ([Consent record and KYA attestation](https://enruta.ai/docs/consent-and-kya)).

## Attest from CI

Inside a GitHub Actions job, `enruta attest workload` requests the job’s OIDC token for the audience `https://api.enruta.ai` and submits it as a `workload_identity` anchor. The job needs permission to request the token:

```yaml
permissions:
  id-token: write
  contents: read

steps:
  - uses: actions/checkout@v4
  - run: npx @enruta/cli attest workload --agent agt_procurement_02
    env:
      ENRUTA_API_KEY: ${{ secrets.ENRUTA_DEVELOPER_KEY }}
```

The CLI reads the Enruta key from `ENRUTA_API_KEY`; `--digest sha256:<hex>` adds a code digest for the token to bind. From another CI, pass its OIDC token in `ENRUTA_WORKLOAD_TOKEN`; Vercel and Google Cloud workloads can also submit their token to `POST /v0/attestations` directly.

## Link a legal entity

- **The user.** When the agent pays with a Stripe Issuing card, a `legal_entity` anchor with `party: user` reads the cardholder and the account’s requirements from Stripe.
- **A platform or a developer.** The organization verifies itself once with `POST /v0/org/verification`: a company publishes the DNS TXT record `enruta-verification=<token>` on its domain; an individual goes through a Stripe Identity session, which runs in Stripe’s test mode today. The anchor then reads that verification.

## Policy: identity requirements

```json
"identity_requirements": [
  { "rails": ["x402"], "require": ["agent_key", "collateral"], "on_fail": "deny" },
  { "above": "1000.00", "require": ["agent_key", "legal_entity:user"], "on_fail": "deny" },
  { "above": "100.00", "require": ["agent_key", "workload_identity"], "on_fail": "step_up" }
],
"payee_identity": { "require_verified_above": "50.00", "on_fail": "step_up" }
```

The first row that matches the payment’s amount and rail applies, so list the strictest rows first. A missing anchor fails the `agent_identity` check with `identity_insufficient`, or with `collateral_insufficient` when the collateral does not cover the amount; `on_fail` decides whether the request steps up or is denied. `payee_identity` does the same for the merchant check, with `payee_unverified`; a checkout whose merchant signature fails is always denied, with `payee_identity_mismatch`.

## Interfaces

| Surface | Call |
|---|---|
| REST | `POST /v0/attestations/nonce`; `POST /v0/attestations` with `{ agent_id, anchor }`; `GET /v0/attestations`; `GET /v0/agents/{id}/identity`; `POST /v0/org/verification` |
| CLI | `enruta attest workload`, `enruta attest key` |
| SDK | `attestations.nonce`, `attestations.create`, `agents.identity`, `org.verification` |
| Records | `agent.identity` on payment records; one `kya_attestation` linked record per anchor |
| Console | The agent page: assurance level, anchors, and how to attest |
