Counterparty Locking
Every beneficiary hashes to a stable identifier. Quarantine new payees, hold compromised ones, verify-once enforces human approval before first use.
A Counterparty is a payee — a bank account, a crypto address, a Meow-internal account. The gateway hashes the wire-level fields into a canonical beneficiary_hash, and every capsule binds that hash. You cannot mint or consume a capsule for a payee the gateway doesn't know about.
Normalized hashing
import { hashBeneficiary } from "@veto/spend-capsule-protocol";
hashBeneficiary({
type: "bank_us",
name: "Acme Corp",
routing: "021000021",
account_last4: "1234"
});
// → "sha256:7ee7f2426cda71548a0fae87c291ff42469358bcb65ff2a0ffaf763d15bae5f4"Per-type normalization:
- bank_us — lowercase name, strip routing whitespace, bind routing + last-4
- bank_intl — uppercase IBAN + country ISO, lowercase name, BIC if present
- crypto (eth/base/arb) — EIP-55 address
- crypto (sol) — base58 address
The hash is stable across TS and Python SDKs — golden vectors in the protocol test suite ensure cross-language parity.
Lifecycle
unverified (quarantined) → verified → (optionally) held → verified- unverified — just seen;
ap_strict_v1requires HITL approval before first use - verified — a human clicked approve; capsules against this hash mint without an extra approval
- held — operator marked the payee as compromised; mints deny immediately
A payee's state is per-org. Two organizations can independently verify the same bank account.
Invoice-swap defense
The hash is the PRIMARY drift anchor. A common attack on naive systems:
- Agent reads invoice saying "pay Acme at routing X, account Y"
- Something swaps the bytes in transit so routing X becomes X'
- Naive system pays the attacker's account
With a counterparty lock:
- Capsule is minted with
counterparty_hash = hash(routing=X, account=Y) - Runtime consume computes
hash(routing=X', account=Y)— different bytes - Drift check fails; the gateway denies + chains a
beneficiary_hash_mismatchreceipt
The receipt survives the attack attempt as audit evidence.
API
# Upsert (computes hash server-side):
POST /v1/counterparties
{
"type": "bank_us",
"display_name": "Acme Corp",
"routing_number": "021000021",
"account_last4": "1234",
"account_holder_name": "Acme Corp",
"operator_id": "op_ap"
}
# Verify (exits quarantine):
POST /v1/counterparties/$HASH/verify
{ "operator_id": "op_compliance" }
# Hold (immediate-reject on drift):
POST /v1/counterparties/$HASH/hold
{ "operator_id": "op_security", "reason": "vendor phishing incident 2026-04-18" }Every verify / hold writes to the decision receipt chain with the operator id + reason, so the audit trail captures WHO trusted WHAT payee WHEN.