Portcullis

An AI agent can propose a payment. It can't make one.

The agent works an accounts-payable inbox over MCP. Every payment it asks for goes to a deterministic policy kernel with no model in it, which allows or refuses it by rule. Only an allowed payment reaches PayPal. Every decision is written to a hash chain, and this page re-checks that chain in your browser.

A script holding the agent's real token tries six ways to steal or short-pay an approved bill. No model, a few seconds. Six different rules refuse it.
  1. 1
    Agent reads mail and calls propose_payment. It never sees the rules.
  2. 2
    Kernel checks payee, amount, bill, limits and balance. Rust, no network, no prompt.
  3. 3
    PayPal Payouts sends only what was allowed, keyed so a retry can't pay twice.
  4. 4
    Audit chain records every step as SHA-256 linked records, verified below.
Gate decisions on record
–
PayPal sandbox
–
Audit chain
loading
Every record re-hashed in this browser, not trusted from the server.
Or let a real model work a phishing inbox:

When the agent reports to Dana, the owner, you answer for her: tell it to pay the attacker, and watch the gate.

One shared demo: everyone on this page watches the same run. Runs are rate-limited because each one moves PayPal sandbox money and calls models. Eight models at once, with you as the attacker: Siege.

Agent

idle
Reading: nothing open
Nothing open yet.
No run yet. Start one above.

Gate decisions

No payment requests yet. Each one the agent makes appears here with the rule that decided it.

Account

Balance
–
Set aside for bills
–
In flight to PayPal
–
Agent may spend
–

Policythe agent can't see or change it

    Approved billsDana's schedule. The agent can pay these, only to the payee shown, only inside the window. It can't add or edit one.

    Audit chain

    Waiting for records Download .jsonl · Server's own check · Audit desk
    How each hash is computed, and how to check it yourself
    hash = SHA-256( "portcullis.audit.v1"
                    ‖ len ‖ seq   ‖ len ‖ at
                    ‖ len ‖ kind  ‖ len ‖ body
                    ‖ len ‖ prev )
    seq, at and every len are u64 big-endian; prev is the previous record's hash (32 zero bytes for record 0).
    Same encoding as crates/kernel/src/audit.rs. This page recomputes every record it receives.
    #TimeKindWhat happenedHash