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.