25attack tests. The limits held.
Payrein holds the agent’s money on NEAR, tested against live Intents code.
| 13:28:28 | Report Shop | 0.002 | Settled report delivered |
| 13:28:38 | attacker.test.near | 0.002 | Refused not allowlisted |
| 13:28:46 | attacker.test.near | 0.002 | Refused leaked key |
| 13:28:52 | Report Shop | 0.008 | Waiting owner approval |
| 13:35:49 | Report Shop | 8 × 0.004 | Settled 7 Refused 1 queue full |
Spent today0.030 of 0.030 wNEAR
After four demo steps on a local chain: one purchase settled, two attacks refused, one payment waiting.
Numbers from the last full run
Local run, 22 October 2026, against the intents.near binary pinned from mainnet block 220928157.
- 27
- unit tests on the contract
cargo test -p policy-vault - 25
- attack tests against the real Intents code
make test-security - 5of 5
- mutations caught; each removed check fails
scripts/mutation-check.py - 0
- access keys on the vault, so nobody can redeploy it
view_access_keys
Four attacks, the same answer
Each runs in the demo with a scripted stand-in for the model, made to act compromised.
The invoice says pay someone else
The merchant’s text moves the payment account to attacker.test.near, and the agent obeys.
Refused MerchantNotAllowed
A plain wallet key pays the attacker.
A script holds the agent’s key
It calls the vault directly, skipping the app. The key can only ask.
Refused MerchantNotAllowed
A plain wallet key is the whole balance.
The app asks the wallet for 0.009
The stored payment is 0.008. The contract compares every field of the approval.
Refused ApprovalMismatch
A swapped payment that is also waiting would pass. Only the wallet’s display of the full call catches that.
Eight requests sent together
Eight keys ask for 0.004 each while one payment still waits. The queue holds eight, so the last is refused.
Settled seven Refused one, TooManyPending
A plain wallet key sends all eight.
Where the money sits
The agent never holds a balance. One restricted key lets it ask; the contract decides.
What a deployment needs
- ToolchainRust 1.93+ (wasm32 target), cargo-near 0.22, Node 22.13+, pnpm 11, Python 3
- Ownera NEAR account; a wallet that shows full call arguments
- Fundsone token balance on NEAR Intents
- Agenta function-call key limited to request_payment
- Testnetabout 19.6 NEAR; it deploys its own copy of Intents
What it does not protect
- The owner’s keyStolen, it changes the policy and withdraws.
- DeliveryA merchant can take payment and not deliver. The limits cap the loss.
- PriceA merchant can quote one price and invoice a higher one within the caps. The contract admits it; the agent’s own check refuses an invoice that differs from its quote and pays nothing.
- Intents adminsCan pause, lock, force-transfer the vault’s balance and upgrade Intents.
- A lost callbackKeeps its slot and day’s reservation until the owner resolves it after ten minutes.
- Midnight UTCThe cap resets; up to twice it can move across the boundary. Accepted.
Built in the open, dated
Work since the 19 October announcement counts, per the organisers’ FAQ; this repository started on 21 October.
| When | What | Check |
|---|---|---|
| Contract, tests, attack suite, merchant, agent, owner app, demo, testnet bootstrap | git history | |
| Owner app layout with numbered steps, evidence rerun, submission kit | evidence/local-run.md | |
| Claims matched to tests, product thesis | docs/threat-model.md | |
| Demo video recorded from the local run | evidence/video-*.png | |
| Repository public, submitted; review findings fixed: bootstrap keys, merchant redelivery; second merchant and the quote check | docs/review-2026-11-02.md | |
| Testnet deployment; every demo check passes on chain | evidence/testnet-run.md | |
| to 14 Nov 2026 | Planned: the wallet check on testnet, real-model run | README, status |
Run it in three commands
A local chain, two merchants, the agent, the owner app. Prerequisites in the README.
pnpm install
make test-security
make demo-local