docs

SPEC sections 2–5

2. Data model (packages/schema)

2.1 Identifiers

- `principal_id`: `did:web:<domain>` (company) or `did:key:` (person). Keys: Ed25519. - `agent_id`: `did:key:z6Mk…` derived from the agent's Ed25519 public key. The same key signs TAP requests. - `mandate_id`: ULID. `evidence_id`: ULID.

2.2 Agent Authority Credential (W3C VC Data Model 2.0)

{
  "@context": ["https://www.w3.org/ns/credentials/v2", "https://prokura.network/ns/v1"],
  "type": ["VerifiableCredential", "AgentAuthorityCredential"],
  "issuer": "did:web:acme.example",
  "validFrom": "2026-11-10T00:00:00Z",
  "validUntil": "2027-02-10T00:00:00Z",
  "credentialStatus": {
    "type": "BitstringStatusListEntry",
    "statusListIndex": "42",
    "statusListCredential": "https://api.prokura.network/status/1"
  },
  "credentialSubject": {
    "id": "did:key:z6Mk…agent",
    "principal": {
      "id": "did:web:acme.example",
      "legalName": "ACME GmbH",
      "register": "HRB 12345 Frankfurt"
    },
    "grantedBy": { "name": "Jane Doe", "role": "CFO", "method": "passkey" },
    "scope": {
      "currency": "EUR",
      "capPerTx": 1500.0,
      "capPeriod": { "amount": 5000.0, "period": "P1M" },
      "fourEyesAbove": 1500.0,
      "payees": [
        { "type": "iban", "value": "DE89370400440532013000", "name": "Supplier AG" },
        { "type": "domain", "value": "cleverbridge.com" }
      ],
      "mcc": ["5734", "7372"],
      "rails": ["mollie", "sepa_batch", "visa_vic"],
      "purposes": ["supplier_invoice", "saas_renewal", "api_usage"]
    },
    "b2b": { "costCentre": "4711", "poRequired": true, "invoiceHashRequired": true, "skontoWindowDays": 14 },
    "delegation": { "subDelegate": false, "maxDepth": 0, "parent": null },
    "evidence": { "retainDays": 3650, "exportFormat": "nachweis/v1" }
  },
  "proof": { "type": "DataIntegrityProof", "cryptosuite": "eddsa-rdfc-2022", "...": "..." }
}

Rules: amounts are decimal strings in the DB (`numeric(18,2)`), numbers in JSON only at the edge. `payees` empty array means "no payee restriction" is **not** allowed in v1: at least one payee or domain is required. `rails` restricts which adapters may execute.

2.3 Payment intent (what the agent submits)

{ "mandateId": "01J…", "agentId": "did:key:z6Mk…",
  "amount": "890.00", "currency": "EUR",
  "payee": { "type": "domain", "value": "cleverbridge.com" },
  "purpose": "saas_renewal",
  "reference": { "po": "PO-2026-118", "invoiceHash": "sha256:…", "description": "Jira 12 seats" },
  "rail": "mollie",
  "protocol": "mcp",          // mcp | ap2 | tap | acp
  "agentSignature": "…"       // Ed25519 over canonical JSON of the intent, by agent key
}

2.4 Decision

{
  "decision": "allow | step_up | deny",
  "reasons": ["CAP_PER_TX_EXCEEDED"],
  "evidenceId": "01J…",
  "stepUp": { "url": "https://approve.prokura.network/a/…", "expiresAt": "…" }
}

Reason codes (closed list): `OK`, `MANDATE_EXPIRED`, `MANDATE_REVOKED`, `MANDATE_NOT_YET_VALID`, `AGENT_MISMATCH`, `SIGNATURE_INVALID`, `PAYEE_NOT_ALLOWED`, `RAIL_NOT_ALLOWED`, `PURPOSE_NOT_ALLOWED`, `CAP_PER_TX_EXCEEDED`, `CAP_PERIOD_EXCEEDED`, `FOUR_EYES_REQUIRED`, `PO_MISSING`, `INVOICE_HASH_MISSING`, `DUPLICATE_INVOICE`, `VOP_MISMATCH` (M3), `MCC_NOT_ALLOWED`.

2.5 Evidence record (Nachweis)

Append-only table `evidence`: `id, prev_hash, hash, mandate_id, agent_id, principal_id, intent_json, intent_hash, decision, reasons[], step_up_json (who approved, passkey credential id, timestamp), rail, rail_ref (e.g. Mollie payment id), rail_status, amount, currency, payee_json, created_at, signature (server Ed25519 over hash)`. `hash = sha256(prev_hash || canonical(intent) || decision || reasons || rail_ref || created_at)`. Export `GET /evidence/{id}/nachweis` → signed JSON + human-readable PDF (M2). Daily Merkle root over the day's hashes published at `GET /evidence/roots/{date}`.

3. HTTP API (apps/api)

Auth: API keys per tenant (hashed), `Authorization: Bearer`. Rate-limited. All bodies Zod-validated. OpenAPI generated at `/openapi.json`.

POST /v1/principals                  create principal (did:web or did:key), returns key material or accepts a public key
POST /v1/agents                      register agent public key -> did:key
POST /v1/mandates                    issue credential (server signs with principal key held in KMS/dev-file) -> VC JSON
GET  /v1/mandates/{id}               fetch (redacted for non-owners)
POST /v1/mandates/{id}/revoke        flips status-list bit, writes evidence
GET  /v1/status/{list}               Bitstring Status List credential
POST /v1/verify                      body: VC -> { valid, errors[] }   (public, no auth)
POST /v1/check                       body: intent -> decision (no execution)
POST /v1/pay                         body: intent -> decision; if allow -> executes via adapter -> evidence with rail_ref
GET  /v1/approvals/{id}              step-up details (for approve app)
POST /v1/approvals/{id}/complete     WebAuthn assertion -> resumes /pay
GET  /v1/evidence/{id}               record
GET  /v1/evidence/{id}/nachweis      signed export
GET  /v1/evidence?mandateId=…        list
GET  /v1/evidence/roots/{date}       daily Merkle root
POST /v1/adapters/ap2/mandate        body: mandateId + cart -> AP2 Intent/Cart/Payment Mandate JSON
POST /v1/adapters/tap/sign           body: method,url,headers -> Signature headers (agent key held client-side preferred; server-side for demo)

4. Policy gate (packages/gate)

Pure function `evaluate(credential, intent, state) -> Decision`, no I/O. `state` = { spentThisPeriod, seenInvoiceHashes, now, revocationBit }. Order of checks is fixed and each failing check appends a reason; `deny` beats `step_up` beats `allow`:

1. signature of intent by `agentId` valid, and `agentId == credentialSubject.id` 2. VC proof valid, issuer key resolves, `validFrom <= now < validUntil`, status bit not set 3. rail ∈ scope.rails; purpose ∈ scope.purposes; payee matches a scope.payees entry (IBAN exact, domain suffix match); MCC if present ∈ scope.mcc 4. b2b: `poRequired` → reference.po present; `invoiceHashRequired` → invoiceHash present and not in seenInvoiceHashes (else `DUPLICATE_INVOICE`) 5. amount ≤ capPerTx else `deny`; spentThisPeriod + amount ≤ capPeriod.amount else `deny` 6. amount > fourEyesAbove → `step_up` 7. else `allow`

Policies must be unit-tested with a table of ≥ 30 cases. Add OPA/Rego support only in M3 if a customer asks.

5. Step-up approval (apps/approve)

Single page: shows principal, agent, amount, payee, purpose, reference, and the reason. Approver authenticates with a passkey registered to the principal (`@simplewebauthn`). On success, `POST /approvals/{id}/complete`; the API resumes `/pay`, records the approver's credential id and time in evidence. Links expire in 15 minutes. Mobile-first. No other UI in v1.