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
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
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
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
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
Anyone can check it
Verify re-reads the order from PayPal and compares custom_id, the amount and the capture with the receipt.
- 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:
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
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.
| API | Endpoints | What for |
|---|---|---|
POST /v3/vault/setup-tokens · POST /v3/vault/payment-tokens | The café saves its PayPal once, without a redirect on later orders. | |
POST /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. | |
GET /v2/payments/captures/{id} | Confirm the capture's status and amount. | |
GET /v1/customer/disputes · GET /v1/customer/disputes/{id} · POST …/provide-evidence · POST …/accept-claim | Import cases, read PayPal's view, send evidence or refund. | |
POST /v1/notifications/verify-webhook-signature | Every incoming event's signature is verified before it is stored. | |
GET /v1/reporting/transactions | Optional: 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_productssearch_catalogcheck_inventorycheck_budgetcompare_market_pricespropose_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_disputeget_receiptget_event_timelineget_paypal_confirmationverify_hashdecide_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.
| Variable | What it does |
|---|---|
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET | Your PayPal sandbox REST app. Required. |
PAYPAL_ENVIRONMENT | sandbox (default). |
LLM_API_KEY, LLM_BASE_URL, LLM_MODEL | Any OpenAI-compatible chat API with tool calling. Without a key a labelled offline fallback runs. |
PAYPAL_WEBHOOK_ID | Optional. Turns on signed webhooks; disputes can always be imported by ID. |
SANDBOX_BUYER_EMAIL | Optional. Lets RelayPay open sandbox disputes for testing. |
CHANNEL3_API_KEY | Optional. Market price check for the agent. |
ALERT_WEBHOOK_URL, NEXT_PUBLIC_BASE_URL | Optional. Owner alerts to a Discord, Slack or ntfy.sh URL, with links back to the app. |
DATABASE_URL | Optional. Postgres (Neon or Render). Without it, data lives in data/db.json. |
PayPal sandbox only. No real money moves.