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
| Level | Description | Bypass Possible? |
|---|---|---|
| Soft enforce | SAT is issued but connector is optional. Agent can pay without SAT. | Yes |
| Hard enforce | Payment credentials are not in the agent runtime. Connector requires SAT verify + atomic consume before executing payment. | No |
Tip
@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
{
"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"
}| Claim | Description |
|---|---|
version | Always 1 |
jti | Unique token ID — prevents replay |
kid | Key ID — selects the signing key for verification |
expiresAt | Unix timestamp — 120 seconds after issuance |
amount/unit | Exact spend amount and currency from the evaluation |
How SAT Flows
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:
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 paymentIntegration: guardedAction
The guardedAction callback receives the authorization result including the 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:
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)
| Scenario | Result |
|---|---|
| Invalid signature | SATVerificationError — payment blocked |
| Expired SAT (> 2 min) | SATVerificationError — payment blocked |
| Already consumed | SATConsumeError (409) — payment blocked |
| Amount/unit mismatch | Cross-check failure — payment blocked |
| Unknown kid | SATVerificationError — payment blocked |
| No SAT provided | Payment 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
API Endpoints
| Endpoint | Description |
|---|---|
POST /spend/evaluate | Returns sat in response when decision is ALLOW |
POST /approvals/:id/resolve | Returns sat when approval is APPROVED |
POST /spend-requests/:id/issue-sat | Re-issue or get existing SAT for an ALLOW decision |
POST /spend-requests/:id/consume-sat | Atomic consume — marks SAT as used. 409 on replay. |