GUIDE

Enforce the grant

Fidacy authorizes; your executor enforces. A verdict on its own changes nothing, a firewall is only real if the code that moves money refuses to move it without a valid grant. That refusal is yours to install, at your PSP boundary, and it is about thirty lines.

The loop

1

Authorize

Your agent calls request_payment (MCP) or /v1/assess (engine). An in-mandate action returns an ALLOW with a short-lived Ed25519 grant; anything outside the mandate returns a DENY with no grant.

2

Enforce

Your executor verifies the grant against Fidacy’s public key and settles exactly once, or refuses. No valid grant, no money.

The executor is your code at your settlement boundary. Fidacy is non-custodial and never touches funds, so the enforcement lives where the money actually moves.

The executor (copy this)

Pure node:crypto, no dependency to install. Swap the mock rail.charge for your PSP call and the single-use Set for a durable, atomic claim in your database.

import { verify as edVerify, createPublicKey } from "node:crypto";

const FIDACY_PUBLIC_KEY_PEM = process.env.FIDACY_PUBLIC_KEY_PEM; // pin it
const redeemed = new Set(); // durable single-use store in production

function verifyGrant(grant, expected) {
  if (typeof grant !== "string" || grant.split(".").length !== 2) return { ok:false, reason:"malformed_grant" };
  const [body, sig] = grant.split(".");
  const pub = createPublicKey(FIDACY_PUBLIC_KEY_PEM);
  const sigOk = edVerify(null, Buffer.from(body,"utf8"), pub, Buffer.from(sig,"base64url"));
  if (!sigOk) return { ok:false, reason:"invalid_signature" };
  const q = JSON.parse(Buffer.from(body,"base64url").toString("utf8"));
  if (Date.now() > q.exp) return { ok:false, reason:"grant_expired" };
  if (q.payee !== expected.payee) return { ok:false, reason:"payee_mismatch" };
  if (q.amount !== expected.amount) return { ok:false, reason:"amount_mismatch" };
  if (q.currency !== expected.currency) return { ok:false, reason:"currency_mismatch" };
  return { ok:true, decisionId:q.decisionId };
}

async function settle(payment, grant, rail) {
  const check = verifyGrant(grant, payment);
  if (!check.ok) return { status:"REFUSED", reason:check.reason };
  if (redeemed.has(check.decisionId)) return { status:"REFUSED", reason:"grant_replayed" };
  redeemed.add(check.decisionId);            // claim BEFORE settling (atomic in your DB)
  const { railRef } = await rail.charge(payment);  // <- Stripe, wire, treasury
  return { status:"EXECUTED", railRef };
}

Prove it does not bypass

Every way a payment can go wrong, a hallucinated payee, a swapped amount, a replayed or forged grant, an outright DENY, produces zero settlements. A single legitimate grant produces exactly one.

await settle(payment, allowGrant, rail);                     // EXECUTED  (one settlement)
await settle(payment, allowGrant, rail);                     // REFUSED   grant_replayed
await settle(payment, undefined, rail);                      // REFUSED   malformed_grant (a DENY carries no grant)
await settle({ ...payment, amount: 900 }, allowGrant, rail); // REFUSED   amount_mismatch
await settle(payment, tamper(allowGrant), rail);             // REFUSED   invalid_signature

This is the same attack matrix Fidacy’s own non-bypass acceptance suite runs in CI. Run it against your executor before you ship, and a regression that would let money slip through fails the build.

What’s in a grant

The grant is <body>.<sig>: body is the base64url of a canonical JSON payload, sig is the Ed25519 signature over that exact body string.

{
  "decisionId": "…",   // single-use key: redeem once, ever
  "subject":    "agent:…",
  "payee":      "acme-supplies.inc",
  "amount":     100,
  "currency":   "USD",
  "exp":        1751560000000,   // epoch ms; refuse after this
  "invoiceRef": "INV-2026-014"   // optional; one payment per invoice
}

Bind every check to the payment actually about to execute. A grant for invoice X and payee A cannot settle payment Y to payee B, even with a perfect signature, the field match is what makes injection and swap attacks dead on arrival.

← PREVIOUS
Payment Firewall
NEXT →
Verify a Payload