Build Your Executor
A PINTI executor is your payment backend. PINTI calls it server-to-server with a signed request when an agent needs to make a payment. Your executor handles the actual payment rail — Stripe, MoonPay, EVM, or anything else.
Note
How It Works
Create a payment handle
Agent calls evaluate
POST /api/v1/spend/evaluate with the payment details. PINTI evaluates the policy and returns a SAT (Spend Authorization Token) if approved.Agent calls execute
POST /api/v1/payment/execute with the handle ID, SAT, and payment parameters. PINTI verifies the SAT, atomically consumes it, then calls your executor.PINTI calls your executor
What PINTI Sends Your Executor
PINTI sends a POST request with JSON body and authentication headers. The contract is rail-agnostic — the same format for Stripe, MoonPay, EVM, or any other payment rail.
Content-Type: application/json
Authorization: <your-auth-token>
X-Pinti-Timestamp: 1708876543
X-Pinti-Signature: a1b2c3d4e5f6... (HMAC-SHA256 hex){
"spendRequestId": "cm1a2b3c4d5e6f7g8h9i",
"satJti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"amountMinor": 5000,
"currency": "usd",
"merchant": "AWS",
"agentId": "agent-purchasing-1",
"description": "Monthly API credits",
"metadata": {
"invoiceId": "INV-001",
"team": "engineering"
}
}| Field | Type | Description |
|---|---|---|
spendRequestId | string | PINTI's internal spend request identifier |
satJti | string (UUID) | Idempotency key. Use this to prevent double charges. |
amountMinor | integer | Amount in smallest currency unit (e.g. 5000 = $50.00) |
currency | string (3-char) | ISO 4217 currency code (lowercase) |
merchant | string | Merchant name as provided by the agent |
agentId | string | Identifier of the requesting agent |
description | string? | Optional payment description |
metadata | object? | Optional key-value pairs (string → string) |
What You Return
{
"success": true,
"transactionId": "ch_1234567890",
"status": "completed"
}{
"success": false,
"errorCode": "INSUFFICIENT_FUNDS",
"errorMessage": "Card declined due to insufficient funds"
}| Field | Type | Description |
|---|---|---|
success | boolean | Whether the payment succeeded |
transactionId | string? | Provider's transaction reference (required on success) |
status | string? | Provider's status label |
errorCode | string? | Machine-readable error code (on failure) |
errorMessage | string? | Human-readable error description (on failure) |
Warning
HMAC Signature Verification
Every request from PINTI includes two headers for authentication:
X-Pinti-Timestamp— Unix timestamp (seconds) of the requestX-Pinti-Signature— HMAC-SHA256 oftimestamp.bodyusing your signing secret
Your executor must:
- Reject requests where the timestamp drifts more than 5 minutes from your server time
- Recompute the HMAC and compare signatures using a timing-safe comparison
- Use the
satJtifield as an idempotency key
Warning
Complete Examples
Node.js (Express)
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
const SIGNING_SECRET = process.env.PINTI_SIGNING_SECRET;
const processedJtis = new Set(); // In production, use Redis/DB
function verifySignature(req) {
const timestamp = req.headers["x-pinti-timestamp"];
const signature = req.headers["x-pinti-signature"];
const body = JSON.stringify(req.body);
// 1. Check timestamp drift (±5 minutes)
const drift = Math.abs(Date.now() / 1000 - Number(timestamp));
if (drift > 300) return false;
// 2. Verify HMAC
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(timestamp + "." + body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
app.post("/pinti-execute", async (req, res) => {
// 1. Verify PINTI signature
if (!verifySignature(req)) {
return res.status(401).json({ error: "Invalid signature" });
}
const { satJti, amountMinor, currency, merchant } = req.body;
// 2. Idempotency check (REQUIRED)
if (processedJtis.has(satJti)) {
return res.json({
success: true,
transactionId: "cached-" + satJti,
status: "idempotent_replay",
});
}
try {
// 3. Execute payment on your rail (Stripe example)
// const charge = await stripe.charges.create({
// amount: amountMinor,
// currency: currency,
// description: `PINTI: ${merchant}`,
// });
processedJtis.add(satJti);
return res.json({
success: true,
transactionId: "tx_" + Date.now(),
status: "completed",
});
} catch (err) {
return res.json({
success: false,
errorCode: "PAYMENT_FAILED",
errorMessage: err.message,
});
}
});
// Health check endpoint (used by PINTI dashboard)
app.get("/health", (req, res) => {
res.json({ status: "healthy" });
});
app.listen(3001, () => console.log("Executor running on :3001"));Python (Flask)
import hmac, hashlib, time, json, os
from flask import Flask, request, jsonify
app = Flask(__name__)
SIGNING_SECRET = os.environ["PINTI_SIGNING_SECRET"]
processed_jtis = set() # In production, use Redis/DB
def verify_signature():
timestamp = request.headers.get("X-Pinti-Timestamp", "")
signature = request.headers.get("X-Pinti-Signature", "")
body = request.get_data(as_text=True)
# Check timestamp drift (±5 minutes)
drift = abs(time.time() - float(timestamp))
if drift > 300:
return False
# Verify HMAC
expected = hmac.new(
SIGNING_SECRET.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.post("/pinti-execute")
def execute():
if not verify_signature():
return jsonify({"error": "Invalid signature"}), 401
data = request.json
sat_jti = data["satJti"]
# Idempotency check (REQUIRED)
if sat_jti in processed_jtis:
return jsonify({
"success": True,
"transactionId": f"cached-{sat_jti}",
"status": "idempotent_replay",
})
try:
# Execute payment on your rail here
processed_jtis.add(sat_jti)
return jsonify({
"success": True,
"transactionId": f"tx_{int(time.time())}",
"status": "completed",
})
except Exception as e:
return jsonify({
"success": False,
"errorCode": "PAYMENT_FAILED",
"errorMessage": str(e),
})
@app.get("/health")
def health():
return jsonify({"status": "healthy"})
if __name__ == "__main__":
app.run(port=3001)Rail-Specific Examples
The executor contract is rail-agnostic. Here are examples of what the payment execution step looks like for different rails:
Stripe
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Inside your /pinti-execute handler:
const charge = await stripe.charges.create({
amount: req.body.amountMinor,
currency: req.body.currency,
description: `PINTI | ${req.body.merchant}`,
metadata: {
pinti_spend_request: req.body.spendRequestId,
pinti_sat_jti: req.body.satJti,
agent_id: req.body.agentId,
},
});
return res.json({
success: true,
transactionId: charge.id,
status: charge.status,
});MoonPay (Crypto On-Ramp)
// Inside your /pinti-execute handler:
const response = await fetch("https://api.moonpay.com/v1/transactions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Api-Key " + process.env.MOONPAY_API_KEY,
},
body: JSON.stringify({
baseCurrencyAmount: req.body.amountMinor / 100,
baseCurrencyCode: req.body.currency,
walletAddress: process.env.DESTINATION_WALLET,
externalTransactionId: req.body.satJti,
}),
});
const tx = await response.json();
return res.json({
success: true,
transactionId: tx.id,
status: tx.status,
});EVM (Ethereum / ERC-20)
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
// Inside your /pinti-execute handler:
const tx = await wallet.sendTransaction({
to: process.env.DESTINATION_ADDRESS,
value: ethers.parseUnits(
String(req.body.amountMinor / 100),
"ether"
),
});
const receipt = await tx.wait();
return res.json({
success: true,
transactionId: tx.hash,
status: receipt.status === 1 ? "confirmed" : "failed",
});Idempotency
The satJti field is a unique identifier for each execution attempt. You must use it as an idempotency key:
- Before executing, check if you have already processed this
satJti - If yes, return the cached result (same
transactionId) - If no, execute the payment and store the result keyed by
satJti
Tip
Error Handling
| Scenario | Your Response | What PINTI Does |
|---|---|---|
| Payment succeeded | { success: true, transactionId: "..." } | Records success, returns to agent |
| Payment declined | { success: false, errorCode: "DECLINED" } | Records failure, SAT stays consumed |
| Your server is down | No response / timeout | 504 to agent, marks as retryable (same SAT JTI can retry) |
| Signature invalid | HTTP 401 | Records as infrastructure error |
Note
Health Check Endpoint
PINTI can monitor your executor's health from the dashboard. Implement a GET /health endpoint on your executor:
{
"status": "healthy"
}
// PINTI checks:
// - Is the endpoint reachable?
// - Does it respond within 5 seconds?
// - Does the JSON contain status: "healthy"?The dashboard shows a health indicator (green/yellow/red) next to each executor handle. You can also trigger a health check manually from Settings.
Deployment Options
Quick Deploy (Public URL)
Deploy your executor to any public hosting (Vercel, Railway, Fly.io, etc.). Fast to set up, but your executor URL is publicly accessible.
- Use HMAC verification to authenticate PINTI requests
- Consider IP allowlisting for additional security
- Good for development and testing
Private Deploy (Recommended for Production)
For true hard enforcement, deploy your executor in a network segment that the agent runtime cannot reach:
- VPC with private networking
- Cloudflare Access / Zero Trust tunnel
- mTLS (mutual TLS)
- IP allowlist restricted to PINTI egress IPs
Warning
Templates
Use one of our starter templates to get running in minutes:
| Template | Rail | Language |
|---|---|---|
| Stripe Executor | Stripe Charges / PaymentIntents | Node.js |
| MoonPay Executor | MoonPay On-Ramp / Swap | Node.js |
| EVM Signer | ETH / ERC-20 Transfers | Node.js |
| Custom Skeleton | Any rail (blank template) | Node.js |
Each template includes HMAC verification, satJti idempotency, error mapping, a Dockerfile, and a one-click deploy button. Check your Settings page for deploy links when creating an External Executor handle.
OpenAPI Specification
A formal OpenAPI 3.0 spec for the executor contract is available at /executor-spec.yaml. Import it into Postman, Swagger UI, or any OpenAPI-compatible tool.
Tip