Veto/docs
Meow Gateway

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

IdentifierMeaning
veto.capsule/1Capsule payload version
veto.receipt/1Receipt payload version
veto.capsule+jwsJWS typ header value
GENESIS_PREV_RECEIPT_HASHsha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Canonicalization (JCS, RFC 8785)

Both capsule and receipt payloads are serialized with JCS before hashing / signing:

  1. Keys sorted lexicographically
  2. Numbers canonicalized (no trailing zeros, no scientific for safe-integer range)
  3. Strings UTF-8 NFC-normalized
  4. 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) input

Verifying

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.