Docs

How RelayPay works

A coffee supplier runs an AI agent that reorders for a café. The café approves each order, RelayPay seals that approval into a receipt, and PayPal keeps the receipt's fingerprint on the payment. When a payment is disputed, the merchant answers from that record.

What RelayPay does

AI agents can now place orders, but when a buyer later says “I never approved that”, the merchant has little to show. RelayPay records the approval at the moment it happens and puts a fingerprint of it into the PayPal payment itself, so there is an independent copy that RelayPay cannot quietly rewrite.

The agent proposes

It reads stock, budget and prices with tools. It never holds PayPal credentials and cannot pay.

The café approves

The approval is sealed into a receipt and fingerprinted with SHA-256.

PayPal keeps a copy

The fingerprint rides on the PayPal order as custom_id.

The flow, step by step

  1. 1

    The café saves its PayPal once

    PayPal Vault stores the café's payment method. RelayPay keeps only the vault token, never card or login details.

  2. 2

    The agent proposes

    The purchasing agent reads stock, the budget and catalog prices with tools, then proposes an order. It has no PayPal credentials and cannot pay.

  3. 3

    The café approves

    The approval, the policy checks that ran and the agent's reasoning are sealed into a receipt and fingerprinted with SHA-256.

  4. 4

    PayPal is charged with the fingerprint

    RelayPay creates a PayPal order with the vault token and puts the fingerprint in custom_id, then captures it. PayPal now holds an independent copy.

  5. 5

    Anyone can check it

    Verify re-reads the order from PayPal and compares custom_id, the amount and the capture with the receipt.

  6. 6

    Disputes are answered from the record

    The dispute agent compares the receipt with PayPal's copy and drafts a response. The merchant decides what, if anything, is sent.

The approval receipt

The receipt is finalised only when the café approves. Its canonical JSON holds the order (intent ID, product, quantity, amount, currency), the agent's rationale and alternatives, who approved and when, the agent ID, model, policy version, prompt hash, the policy checks that ran, whether a high-value order was confirmed, and the Channel3 market check when one ran.

RelayPay hashes that exact text with SHA-256. The 64-character hex result is the receipt's fingerprint and is sent to PayPal as the order's custom_id. Change one character of the receipt and the fingerprint no longer matches PayPal's copy.

Verify a receipt yourself

Open a receipt's public page (Verify a receipt), expand Canonical receipt and copy the text exactly. Then:

TSverify.mjs
import { createHash } from "crypto";
const text = `…canonical receipt…`;
const hash = createHash("sha256").update(text).digest("hex");
// equal to the receipt hash, and to PayPal's custom_id
Compare with purchase_units[0].custom_id from GET /v2/checkout/orders/{id}.

The merchant can read custom_id from the PayPal order with the Orders API. RelayPay's Verify with PayPal button does exactly that, and also checks the amount and the capture.

PayPal APIs used

All calls go from RelayPay's server. The agents call RelayPay tools, never PayPal directly.

APIEndpointsWhat for
Vault v3POST /v3/vault/setup-tokens · POST /v3/vault/payment-tokensThe café saves its PayPal once, without a redirect on later orders.
Orders v2POST /v2/checkout/orders · POST /v2/checkout/orders/{id}/capture · GET /v2/checkout/orders/{id}Charge with vault_id and custom_id = receipt fingerprint; re-read it to verify.
Payments v2GET /v2/payments/captures/{id}Confirm the capture's status and amount.
Disputes v1GET /v1/customer/disputes · GET /v1/customer/disputes/{id} · POST …/provide-evidence · POST …/accept-claimImport cases, read PayPal's view, send evidence or refund.
WebhooksPOST /v1/notifications/verify-webhook-signatureEvery incoming event's signature is verified before it is stored.
Transaction SearchGET /v1/reporting/transactionsOptional: reconcile captures against PayPal's reporting ledger (ENABLE_RECONCILE, needs Transaction search on the PayPal app; off on the hosted demo).

The two AI agents

Both agents use OpenAI-compatible tool calling. Every tool call is recorded with its input, output, time and source (model or fallback), and shown in the console. If no LLM key is set, a deterministic fallback runs and every step is labelled as fallback.

Purchasing agent

Proposes the café's next order from the supplier catalog. RelayPay recomputes every price and checks the café's rules again before charging.

  • get_low_stock_products
  • search_catalog
  • check_inventory
  • check_budget
  • compare_market_prices
  • propose_purchase

Dispute agent

Reads the PayPal case, compares the receipt with PayPal's copy and drafts a response. It recommends contesting only when the fingerprint matches PayPal.

  • get_dispute
  • get_receipt
  • get_event_timeline
  • get_paypal_confirmation
  • verify_hash
  • decide_case

Channel3, alerts and the stack

  • Channel3 gives the purchasing agent a market price check: the supplier's price against the median of live retail offers. The result is sealed into the receipt.
  • Owner alerts go to Discord, Slack or ntfy.sh (a Zapier Catch Hook also works) when an order needs approval, a payment is captured, or a dispute arrives or is answered.
  • AG Grid draws the receipts and ledger tables, with search, sorting and CSV export.
  • Next.js on Render, with Postgres on Neon in production.

What a receipt does and doesn't show

When a receipt matches PayPal's copy, it shows the receipt existed at payment time and has not changed since: it is tamper-evident, and PayPal holds an independent copy of its fingerprint. It records who RelayPay says approved, and when.

It does not prove who was physically at the keyboard, and it does not decide a dispute. PayPal decides; RelayPay strengthens the merchant's response.

Run it locally

Node 20 or later. Clone the repository, run npm install, create .env from .env.example, then npm run dev and open localhost:3000. The console is at /app.

VariableWhat it does
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRETYour PayPal sandbox REST app. Required.
PAYPAL_ENVIRONMENTsandbox (default).
LLM_API_KEY, LLM_BASE_URL, LLM_MODELAny OpenAI-compatible chat API with tool calling. Without a key a labelled offline fallback runs.
PAYPAL_WEBHOOK_IDOptional. Turns on signed webhooks; disputes can always be imported by ID.
SANDBOX_BUYER_EMAILOptional. Lets RelayPay open sandbox disputes for testing.
CHANNEL3_API_KEYOptional. Market price check for the agent.
ALERT_WEBHOOK_URL, NEXT_PUBLIC_BASE_URLOptional. Owner alerts to a Discord, Slack or ntfy.sh URL, with links back to the app.
DATABASE_URLOptional. Postgres (Neon or Render). Without it, data lives in data/db.json.

PayPal sandbox only. No real money moves.