PINTI Docs

Spend Authorization Tokens (SAT)

Cryptographic proof that PINTI authorized a payment. Ed25519-signed, time-limited, single-use.

What is SAT?

A SAT is a cryptographically signed token issued by PINTI when a spend evaluation returns ALLOW. It proves that the policy engine approved the specific spend intent — including amount, unit, merchant, and execution mode.

SATs are Ed25519-signed, expire in 2 minutes, and can only be consumed once. They enable hard enforcement: payment providers can verify that PINTI approved the transaction before executing it.

Enforce Levels

LevelDescriptionBypass Possible?
Soft enforceSAT is issued but connector is optional. Agent can pay without SAT.Yes
Hard enforcePayment credentials are not in the agent runtime. Connector requires SAT verify + atomic consume before executing payment.No

Tip

For hard enforcement, you have two options: run @pinti/stripe-connector on your own backend (Model 1), or let PINTI execute payments via Stripe Connect (Model 2). See Execution Modes for a full comparison.

Token Structure

Format: base64url(payload).base64url(Ed25519_signature)

Payload Claims

SATPayload
{
  "version": 1,
  "workspaceId": "cm...",
  "spendRequestId": "cm...",
  "agentId": "my-agent",
  "amountMinor": 5000,
  "unit": "USD",
  "merchantNormalized": "openai.com",
  "executionMode": "sdk",
  "issuedAt": 1740000000,
  "expiresAt": 1740000120,
  "jti": "a1b2c3d4e5f6...",
  "kid": "k1a2b3c4"
}
ClaimDescription
versionAlways 1
jtiUnique token ID — prevents replay
kidKey ID — selects the signing key for verification
expiresAtUnix timestamp — 120 seconds after issuance
amount/unitExact spend amount and currency from the evaluation

How SAT Flows

End-to-end flow
1. Agent calls PINTI → POST /spend/evaluate
2. PINTI evaluates policy → ALLOW
3. PINTI mints SAT → returns { decision, sat }
4. Agent passes SAT to backend
5. Backend: verify SAT → cross-check → consume SAT → execute payment
6. Done. SAT is consumed. Cannot be replayed.

Integration: SDK

The guard() function automatically returns the SAT when the decision is ALLOW:

@pinti/guard
import { PintiGuard } from "@pinti/guard";

const pinti = new PintiGuard({ apiKey: process.env.PINTI_API_KEY! });

const auth = await pinti.authorize({
  agentId: "agent-1",
  amountMinor: 5000,  // $50.00 in cents
  currency: "usd",
  merchant: "openai.com",
  category: "api",
  reason: "Monthly credits",
});

console.log("SAT:", auth.sat);
// Pass auth.sat to submitReceipt() after payment

Integration: guardedAction

The guardedAction callback receives the authorization result including the SAT:

guardedAction with SAT
const result = await pinti.guardedAction(
  { agentId: "agent-1", amountMinor: 5000, currency: "usd",
    merchant: "openai.com", category: "api", reason: "Credits" },
  async (auth) => {
    // auth.sat is the authorization token
    const charge = await stripe.charges.create({ amount: 5000, currency: "usd" });
    return {
      railId: "stripe",
      transactionId: charge.id,
      actualAmountMinor: charge.amount,
      actualCurrency: charge.currency,
    };
  }
);

Integration: Stripe Connector

For hard enforcement, use @pinti/stripe-connector on your backend:

Stripe Connector
import { createPintiStripe } from "@pinti/stripe-connector";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

const pintiStripe = createPintiStripe({
  stripe,
  getPublicKey: (kid) => getWorkspacePublicKey(kid),
  consumeSat: async (sat) => {
    // Call PINTI consume endpoint
    const res = await fetch(
      `https://pinti.ai/api/v1/spend-requests/${id}/consume-sat`,
      { method: "POST", headers: { "x-api-key": PINTI_KEY },
        body: JSON.stringify({ sat }) }
    );
    return res.json();
  },
});

// This will: verify → cross-check → consume → Stripe call
const pi = await pintiStripe.createPaymentIntent(sat, {
  amount: 5000,
  currency: "usd",
});

Failure Modes (Fail-Closed)

ScenarioResult
Invalid signatureSATVerificationError — payment blocked
Expired SAT (> 2 min)SATVerificationError — payment blocked
Already consumedSATConsumeError (409) — payment blocked
Amount/unit mismatchCross-check failure — payment blocked
Unknown kidSATVerificationError — payment blocked
No SAT providedPayment blocked (connector requires SAT)

Key Rotation

Each workspace has a key registry. SATs include a kid claim that identifies which key signed them. Old keys remain valid for verification during a grace period, allowing seamless rotation without downtime.

Warning

Known limitation: Private keys are encrypted at rest using AES-256-GCM with a single MASTER_KEY environment variable. MASTER_KEY compromise would expose all workspace private keys. For production deployments, consider KMS, envelope encryption, or per-workspace key wrapping.

API Endpoints

EndpointDescription
POST /spend/evaluateReturns sat in response when decision is ALLOW
POST /approvals/:id/resolveReturns sat when approval is APPROVED
POST /spend-requests/:id/issue-satRe-issue or get existing SAT for an ALLOW decision
POST /spend-requests/:id/consume-satAtomic consume — marks SAT as used. 409 on replay.