FurlPay Docs
Open App
  • Services & Availability
  • Quickstart
  • For AI Agents
  • Monorepo
  • API Routes
  • Authenticationupdated
  • Webhook Eventsupdated
  • Error Codesupdated
  • Rate Limitsnew
  • Configuration & Setupnew
  • Merchant Paymentsnew
  • Payroll & Payoutsnew
  • Agentic Payments (x402)updated
  • Agent Trust & Mandates (TAP)updated
  • CCTP Cross-Chainupdated
  • LI.FI Swapsupdated
  • FurlPay Travels (Travel MCP)updated
  • Solana Actions & Blinks
  • Settlement Workspacenew
  • FURL Tokennew
  • Claude Connectorupdated
  • AI Assistants
  • Circlenew
  • Ramp Providersnew
  • Telegram Mini Appnew
  • Stripe Crypto
  • Persona KYC
  • Regulatory status
  • Screening & Casesnew
  • Security Postureupdated
  • Signing & WebAuthnupdated
  • Help Center
  • Getting Started
  • KYC Verification
  • Passkeys & Biometrics
  • Privacy & Data Protection
  • Transaction Statuses
  • Gasless Transfers
  • Deposits & Withdrawals
  • Managing Virtual Cards
  • Freezing & Unfreezing Cards
  • Declined Transactions
  • SDK & API Support
  • x402 Monetization Basics
  • Booking Travel
  • Travel Refunds & Cancellations

Resources

  • Changelog
  • System Status
  • OpenAPI Spec
  • Community
  • GitHub
Docs/Payments/Agent Trust & Mandates (TAP)

Payments

Agent Trust & Mandates (TAP)

An AI agent that can spend money needs more than a payment rail — it needs provable permission. @furlpay/agent-trust is a developer library for agent identity, signed spending mandates, replay-aware intent tokens and request signatures. It supplies controls for an integration to evaluate; it does not itself execute or settle payments.

Library support does not enable a FurlPay payment rail

FurlPay agent payment settlement is currently in development. The travel and card examples below illustrate how a developer might bind an action to a mandate; production travel booking and card issuing are unavailable. Check the service overview and package release notes before relying on an API shown here.

The trust chain

1 · Identity
generateKeypair()Ed25519 · uk_ / ak_ SPKI fingerprints
2 · Consent
issueMandate()user signs: cap · MCC · expiry · single-use
3 · Intent
createBookingToken()agent signs ONE amount/mcc/source + nonce
4 · Verify
trust.verifyBookingToken()chain + constraints + replay → credential
User key signs the mandate → mandate names one agent key → agent key signs one exact intent → verifier checks the whole chain before any credential is issued.

The design has one central invariant: an agent can never widen its own permissions. Only the user's key can sign a mandate, and the verifier refuses any execution whose parameters differ — even by one cent or one merchant-category code — from what the agent signed.

How an intent token is judged

Verification is a strict decision tree — the first failed check refuses the token. A valid token is evidence for an integrating service; it is not a payment, booking confirmation or settlement receipt:

Rendering diagram…
Mandate evaluation: every gate must pass before a single-use credential is issued.

User-signed spend mandates

A mandate binds one agent key to explicit constraints. It is a base64url payload signed by the user's Ed25519 key — tamper with any field and the signature fails:

typescript
import { AgentTrust, generateKeypair, issueMandate } from "@furlpay/agent-trust";

const user  = generateKeypair("user");   // uk_…
const agent = generateKeypair("agent");  // ak_…

const mandate = issueMandate({
  userPrivateKeyPem: user.privateKeyPem,
  userPublicKeyPem:  user.publicKeyPem,
  agentKeyId: agent.keyId,               // this ONE agent, no other
  constraints: {
    maxTotalUsd: 500,                    // spend allowance across all bookings
    maxPerBookingUsd: 400,               // per-transaction ceiling
    mccAllowlist: ["7011", "4511"],      // lodging + airlines only
    sourceAllowlist: ["travala", "legacy"],
    expiresAt: new Date(Date.now() + 7 * 864e5).toISOString(),
    singleUse: false,                    // true → consumed by first booking
  },
});

Booking tokens: one signed intent

For each payment the agent wraps the mandate around one concrete intent and signs it with its own key. The token carries a fresh nonce, so it clears verification exactly once:

typescript
import { createBookingToken } from "@furlpay/agent-trust";

const token = createBookingToken({
  mandate,
  agentPrivateKeyPem: agent.privateKeyPem,
  agentPublicKeyPem:  agent.publicKeyPem,
  intent: { amountUsd: 320, source: "legacy", mcc: "7011", merchant: "Hotel CDMX" },
});

// Rail side (MCP server, API, x402 facilitator):
const trust = new AgentTrust();
trust.registerUser(user.publicKeyPem);
trust.registerAgent(agent.publicKeyPem);

const decision = await trust.verifyBookingToken(token, { amountUsd: 320, mcc: "7011", source: "legacy" });
// → { ok: true, agentKeyId: "ak_…", mandateId: "mnd_…", remainingUsd: 180 }

Verification is stateful and fails closed. In order, the verifier checks:

  • Agent identity — the agent key is registered and really signed this intent.
  • User consent — the mandate verifies against a registered user key and names this agent.
  • Claims equality — the amount/mcc/source the rail is about to execute equal the signed intent byte-for-byte.
  • Constraints — total cap (budgets decrement on success), per-booking cap, MCC and source allowlists, expiry, single-use consumption.
  • Replay — the nonce is burned atomically; a replayed token is rejected and reported as such.

RFC 9421 HTTP signatures

The same identity keys authenticate raw HTTP calls — TAP's transport binding. signRequest() produces Signature-Input, Signature and Content-Digest headers over @method, @target-uri and the body digest, with tag="agent-payment":

typescript
import { signRequest } from "@furlpay/agent-trust";

const body = JSON.stringify({ action: "prepare", amountUsd: 180, purpose: "example only" });
const headers = signRequest({
  method: "POST",
  url: "https://merchant.example/api/agent-actions",
  body,
  keyId: agent.keyId,
  privateKeyPem: agent.privateKeyPem,
});

await fetch("https://merchant.example/api/agent-actions", {
  method: "POST",
  headers: { "content-type": "application/json", ...headers },
  body,
});

Capability tokens: per-call, one server, once

A mandate says what an agent may spend; a capability says which tool-server it may call, for how long, and exactly once. It is the third tier of the multi-tier session model — a short-lived, audience-restricted token the agent mints per call, so an intercepted token can't be replayed by another holder or against another server:

typescript
import { mintCapability } from "@furlpay/agent-trust";

// Agent mints a single-use capability for ONE tool-server call:
const cap = mintCapability({
  agentPrivateKeyPem: agent.privateKeyPem,
  agentPublicKeyPem:  agent.publicKeyPem,
  audience: "https://your-tool-server.example", // valid at this server only
  action:   "prepare:payment",
  maxUsd:   400,
  ttlSeconds: 120,                               // minted per call, not held
});

// Tool-server side — accepts the token only when every check passes:
const d = await trust.verifyCapability(cap, {
  audience: "https://your-tool-server.example", // must be THIS server
  action:   "prepare:payment",
  presenterKeyId: verified.keyId,                // PoP: same key that minted it
  maxUsd:   1000,                                // server ceiling
});
// → { ok: true, agentKeyId, jti, audience, action, maxUsd } — and the jti burns

verifyCapability rejects, with a specific reason, any token that:

  • Wrong audience — minted for a different tool-server (no cross-server replay).
  • Wrong presenter — presented by a key other than the one that minted it (proof-of-possession; pair with verifyRequest to bind the request to that key).
  • Expired, action mismatch, or a maxUsd above the server ceiling.
  • Replayed — the jti burns on first success via the same nonce store as bookings.

Together the tiers form one chain: mandate (user consent + budget) → request signature (who is calling now) → capability (this call, this server, once).

FurlPay agent route boundaries

FurlPay exposes agent-key management and an escrow-hold route in the codebase. These routes do not establish a live card or payment product, and the escrow route can return 503 service_unavailable when durable hold storage is not configured:

  • POST /api/agents/keys — register an agent's Ed25519 public key (SPKI PEM). Returns a key identifier for the agent's public key. When registered to an account, the key can carry a spend policy with a transaction limit, daily budget and category allowlist. Requests to guarded endpoints may be checked against that policy. GET lists owned keys; DELETE revokes a key. Check each route response for the result.
  • POST /api/escrow — an implementation of create/capture/release hold operations. It is not a card authorization or an on-chain settlement service. Its backing store must be durable for the route to work; check the response and do not promise a hold from a successful signature alone.

Package and integration status

The repository may contain APIs that are newer than the public npm release. Check the package's published version and changelog before importing a method shown here. Review settlement status separately in the service overview.
Did this page help?
Edit this page on GitHub

← Previous

Agentic Payments (x402)

Next →

CCTP Cross-Chain