PINTI Docs

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

This guide covers the External Executor mode. If your agent holds its own payment credentials, use SDK Guard instead. For managed execution where PINTI owns the payment, see Managed Mode (coming soon).

How It Works

1

Create a payment handle

In Settings → Payment Handles, create an External Executor handle. Provide your executor URL and auth token. PINTI encrypts these at rest. You receive a signing secret (shown once) for HMAC verification.
2

Agent calls evaluate

Your agent calls POST /api/v1/spend/evaluate with the payment details. PINTI evaluates the policy and returns a SAT (Spend Authorization Token) if approved.
3

Agent calls execute

Agent calls POST /api/v1/payment/execute with the handle ID, SAT, and payment parameters. PINTI verifies the SAT, atomically consumes it, then calls your executor.
4

PINTI calls your executor

PINTI sends a signed POST request to your executor URL. Your backend verifies the HMAC signature, processes the payment on any rail, and returns the result.

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.

Request headers
Content-Type: application/json
Authorization: <your-auth-token>
X-Pinti-Timestamp: 1708876543
X-Pinti-Signature: a1b2c3d4e5f6...  (HMAC-SHA256 hex)
Request body
{
  "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"
  }
}
FieldTypeDescription
spendRequestIdstringPINTI's internal spend request identifier
satJtistring (UUID)Idempotency key. Use this to prevent double charges.
amountMinorintegerAmount in smallest currency unit (e.g. 5000 = $50.00)
currencystring (3-char)ISO 4217 currency code (lowercase)
merchantstringMerchant name as provided by the agent
agentIdstringIdentifier of the requesting agent
descriptionstring?Optional payment description
metadataobject?Optional key-value pairs (string → string)

What You Return

Success response (200)
{
  "success": true,
  "transactionId": "ch_1234567890",
  "status": "completed"
}
Business error response (200)
{
  "success": false,
  "errorCode": "INSUFFICIENT_FUNDS",
  "errorMessage": "Card declined due to insufficient funds"
}
FieldTypeDescription
successbooleanWhether the payment succeeded
transactionIdstring?Provider's transaction reference (required on success)
statusstring?Provider's status label
errorCodestring?Machine-readable error code (on failure)
errorMessagestring?Human-readable error description (on failure)

Warning

Return HTTP 200 for both success and business errors. PINTI interprets non-200 HTTP status codes as executor-level failures (infrastructure error, not payment error).

HMAC Signature Verification

Every request from PINTI includes two headers for authentication:

  • X-Pinti-Timestamp — Unix timestamp (seconds) of the request
  • X-Pinti-Signature — HMAC-SHA256 of timestamp.body using your signing secret

Your executor must:

  1. Reject requests where the timestamp drifts more than 5 minutes from your server time
  2. Recompute the HMAC and compare signatures using a timing-safe comparison
  3. Use the satJti field as an idempotency key

Warning

MUST: Use satJti as idempotency key. If PINTI crashes after your executor succeeds, PINTI will retry with the same satJti. Without idempotency on your side, this will cause double charges. This is not optional.

Complete Examples

Node.js (Express)

executor.js — ~60 lines, copy-paste ready
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)

executor.py — Flask equivalent
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

Stripe charge inside your executor
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)

MoonPay purchase inside your executor
// 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)

EVM transfer inside your executor
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:

  1. Before executing, check if you have already processed this satJti
  2. If yes, return the cached result (same transactionId)
  3. If no, execute the payment and store the result keyed by satJti

Tip

For production, store processed JTIs in a database or Redis with a TTL of at least 24 hours. The in-memory Set in the examples above is for illustration only.

Error Handling

ScenarioYour ResponseWhat 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 downNo response / timeout504 to agent, marks as retryable (same SAT JTI can retry)
Signature invalidHTTP 401Records as infrastructure error

Note

Network errors (timeout, connection refused) are retryable. PINTI does NOT mark the SAT as fully consumed on network errors, so the agent can retry with the same SAT. This is why idempotency is critical.

Health Check Endpoint

PINTI can monitor your executor's health from the dashboard. Implement a GET /health endpoint on your executor:

GET /health response
{
  "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

Hard enforcement requires private executor networking. If your agent can reach the executor URL directly, it could bypass PINTI entirely. HMAC verification prevents forged requests, but does not prevent the agent from making its own requests if it knows the auth token.

Templates

Use one of our starter templates to get running in minutes:

TemplateRailLanguage
Stripe ExecutorStripe Charges / PaymentIntentsNode.js
MoonPay ExecutorMoonPay On-Ramp / SwapNode.js
EVM SignerETH / ERC-20 TransfersNode.js
Custom SkeletonAny 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

The executor spec is rail-agnostic. The same request/response format works for any payment rail. Your executor decides what to do with the payment parameters — PINTI doesn't care which provider you use.