Protocol reference
veto.capsule/1 and veto.receipt/1 — schemas, canonicalization rules, golden vectors.
The Spend Capsule Protocol is published as @veto/spend-capsule-protocol on npm (Apache-2.0). It defines two schemas, two signing flows, and the canonicalization rules that make everything reproducible across languages.
Versions
| Identifier | Meaning |
|---|---|
veto.capsule/1 | Capsule payload version |
veto.receipt/1 | Receipt payload version |
veto.capsule+jws | JWS typ header value |
GENESIS_PREV_RECEIPT_HASH | sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
Canonicalization (JCS, RFC 8785)
Both capsule and receipt payloads are serialized with JCS before hashing / signing:
- Keys sorted lexicographically
- Numbers canonicalized (no trailing zeros, no scientific for safe-integer range)
- Strings UTF-8 NFC-normalized
- No whitespace in the output
The canonicalize() helper in the protocol package is the authoritative implementation. Python + TypeScript both emit byte-identical output for the same input.
Capsule payload
interface CapsulePayload {
version: "veto.capsule/1";
capsule_id: string; // cap_<24 hex>
issuer: string;
entity_id: string;
agent_id: string;
session_id?: string;
tool: string; // exact match at consume
rail_allowlist: Rail[]; // ach | wire | international_wire | book | usdc.*
counterparty_hash: string; // sha256:<hex>
amount_ceiling: { currency: string; amount: string };
memo_template?: string;
invoice_hash: string; // sha256:<hex>
workflow_id: string; // wf_<24 hex>
policy_sha256: string; // bare hex, no prefix
approval_ref?: string | null; // apr_<24 hex>
dual_control_ref?: string | null;
issued_at: string; // RFC3339 Z, no fractional seconds
expires_at: string;
max_uses?: number; // pinned to 1
nonce: string;
}Receipt payload
interface ReceiptPayload {
version: "veto.receipt/1";
receipt_id: string;
entity_id: string;
agent_id: string;
session_id?: string;
workflow_id?: string;
capsule_id?: string | null;
tool: string;
decision: "allow" | "deny" | "require_approval";
reason_code?: string;
reason_detail?: string;
args_hash: string; // sha256:<hex>
result_hash?: string | null;
approval_hash?: string | null;
policy_hash: string; // bare hex
policy_pack_id?: string;
counterparty_hash?: string | null;
rail?: string | null;
amount?: { currency: string; amount: string } | null;
issued_at: string;
prev_receipt_hash: string; // sha256:<hex>
merkle_root: string; // sha256:<hex>
}Signing
import { signCapsule, publicJwkFromPrivate } from "@veto/spend-capsule-protocol";
const jws = await signCapsule(payload, privateKey);
// Header: { alg: "EdDSA", kid, typ: "veto.capsule+jws" }
// Body: JCS-canonical payload, base64url-encoded
// Signature: Ed25519 over the base64url(header).base64url(body) inputVerifying
import { verifyCapsule } from "@veto/spend-capsule-protocol";
const result = await verifyCapsule(jws, jwks, {
now: Date.now(),
skewToleranceMs: 30_000
});
if (!result.ok) {
// result.reason is one of the ErrorCode values (see /meow/errors)
}Golden vectors
The protocol package ships test/fixtures with golden JWS outputs for every variant. Any implementation that produces different bytes for the same input is non-conformant. File location: veto/packages/spend-capsule-protocol/test/fixtures/.
Python parity
from veto.spend_capsule_protocol import sign_capsule, verify_capsule, hash_beneficiary
jws = sign_capsule(payload, private_key_pkcs8_pem)
result = verify_capsule(jws, jwks_json)TS and Python canonicalize identically, so a capsule signed by the TS gateway verifies in Python and vice versa. The test suite includes cross-language contract tests that prove this on every release.