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.
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.
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.
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 };
}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_signatureThis 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.
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.