Inspiration

AI agents are starting to spend money. A prompt injection or a buggy model can change a payee, raise an amount or quietly add an item. We wanted a small, checkable gate between the agent and PayPal.

What it does

  • The agent (NVIDIA Nemotron via Nebius Token Factory) builds a cart from a merchant catalog.
  • A person approves the exact cart. The gate hashes the canonical order and issues a single-use permit.
  • The agent can pay only with that permit and that exact order. A changed amount, payee or item, or a replay, is blocked before any PayPal request.
  • The PayPal order carries the approved hash in custom_id. Before capture, the gate reads the order back from PayPal and captures only if it still matches.
  • Every allowed and blocked attempt writes an Ed25519 signed, hash-chained receipt. verify.py checks the chain offline with the public key.

How we built it

Python, FastAPI, httpx, PayPal Orders v2 (sandbox), Ed25519 receipts, SQLite permits. Prices come from the catalog, never from the model.

Live sandbox evidence (in the repo, evidence/)

  • Order 4CY72468DE151393K: created, four tampering attempts (raised amount, swapped payee, added item, permit replay) blocked with zero PayPal calls, read-back refused capture while unapproved, buyer approved on PayPal, gate re-checked and captured (capture 3M2115364G941974K, COMPLETED, 30.50 USD). 11 receipts VALID.
  • Order 6YN25222V39712547: created with return and cancel URLs, buyer approved on PayPal and landed on the return page (which never captures), gate read the order back, it matched, and the gate captured (capture 71V442751H327834G, COMPLETED, 30.50 USD). 3 receipts VALID.
  • 72 unit tests pass on Python 3.11 and 3.12; CI uses no credentials. All PayPal calls went to the sandbox. No PayPal Live calls. No buyer data is stored in the evidence.

Try it

git clone https://github.com/TheSingulant/ai4-pay-gate-public
cd ai4-pay-gate-public && pip install -r requirements.txt pytest
python -m pytest -q
python scripts/demo_offline.py
python verify.py evidence/live-20261030T115405Z/receipts.jsonl --pubkey="$(cat evidence/live-20261030T115405Z/public_key.txt)"

Challenges

Getting the buyer redirect right (experience_context return_url, cancel_url, PAY_NOW) and keeping the return page from ever capturing on its own.

What we learned

Binding approval to a hash of the canonical order is simple and catches every tampering case we tried.

What's next

Webhook verification, multi-item partial refunds with receipts, and a hosted demo.

Built With

Share this project:

Updates

Submission history