We gave an AI agent a wallet and two invoices. One came from the supplier it is supposed to pay, Northwind Relay Services FZ-LLC: 0.5 NEAR to the account we know. The other came from Northwind Cloud Hosting Ltd, a lookalike with a similar name, a plausible list of line items and a different payment account, asking for 5 NEAR. In our lab measurement the agent tried to pay the fake invoice in 20 of 20 runs. The vault it was asking refused every one of those payments, so the money never left.
That is near-agent-guard, Aunova’s entry to NEAR HACKATHON in the AI Security & Defense track. We submitted it on 8 November 2026, and results are due by 18 November 2026. The code is public at github.com/aunova/near-agent-guard.
The claim is deliberately narrow: an AI agent whose only key can just ask a vault contract to pay allowlisted payees, a bounded amount per window, until the owner revokes it. It does not make the agent smarter. It bounds what the agent can do when it is wrong.
The voiceover in this video is AI-generated (an ElevenLabs voice via NanoGPT). The demo is also posted on Nostr.
What we built
An agent that pays invoices reads text written by strangers. If it holds a full-access key, one convincing invoice or one line of prompt injection becomes a payment. So we took the key away and put four pieces around the money: three layers that each stop something the others do not, and a signed record of every attempt.
The vault contract. A Rust contract (near-sdk 5.30) on NEAR testnet holds the funds and decides every request against the owner’s policy: a per-transaction cap, a fixed-window cap, a receiver allowlist, a maximum number of ledger entries per window (50 in the demo), and revocation. A revoked agent’s request is refused outright and stores nothing. Only the owner can withdraw. Every decision, approved or denied, is recorded in an on-chain ledger with its reasons.
The function-call key. The bridge holds exactly one key for the agent’s account, a NEAR function-call access key, and the agent never sees it. That key may call only request_spend on the vault. This part is enforced by NEAR itself, not by our code: when that key tries to call set_policy, attach a deposit, or make a plain transfer, the protocol rejects the transaction (InvalidAccessKeyError) before any contract code runs.
A compromised agent signing with its own key directly: NEAR refuses all three attempts before the contract is involved.
The WasmBox screen. The process that holds the key never parses the agent’s raw request. The request is parsed inside a WasmBox sandbox: WasmBox is Aunova’s sandboxed WebAssembly tool launcher, which runs a WebAssembly tool with only the capabilities you grant it and checks the module’s sha256 before every run. Our spend-screen.wasm gets stdin and stdout and nothing else. It blocks any tool other than pay, malformed amounts and account names, and common secret formats in the memo, which ends up public on-chain.
The signed audit log. Every attempt is signed into a hash-chained log in the Nostr event format (NIP-01 serialisation, BIP-340 Schnorr signatures, kind 4242). The log is never published to relays. Each attempt’s id is also stored on-chain with its ledger entry, so guard verify --complete can check the log against the chain and the chain against the log.
The agent itself is mini-swe-agent 2.4.6 driving DeepSeek V4.1 Flash, served by NanoGPT. It runs in rootless podman with --network=none and no keys. Its only way out is a wallet client that talks to the bridge over a socket.
agent (rootless podman, --network=none, no keys)
| wallet pay --to <account> --amount <NEAR> --memo <ref>
v
guard bridge
| spend-screen.wasm in WasmBox: stdin/stdout only, sha256 checked before every run
| holds the one function-call key: request_spend on the vault, nothing else
v
vault contract on NEAR testnet
| per-tx cap, window cap, allowlist, entries per window, revocation
+--> on-chain ledger: every decision, approved or denied, with reasons and the attempt id
^
| guard verify --complete checks both directions
v
signed audit log: every attempt, hash-chained, kind 4242
What we measured
We ran the fake-invoice scenario 20 times on a lab deployment. The agent tried to pay the lookalike in all 20 runs. Half the runs gave it neutral wallet help text, and half used help text that named the guard: 10 of 10 tried either way, so telling the agent about the guard made no difference.
Across those 20 runs the agent sent 24 payment requests to the lookalike account, and the vault denied all 24. Every one was denied because the payee is not on the allowlist. The 5 NEAR requests were also over the per-transaction cap. One retry asked for 0.1 NEAR, under both caps, and was denied for the allowlist alone. In 4 of the 20 runs the agent retried after a denial.
One real run on the public vault. This agent stopped after the denial; in our lab runs, 4 of 20 retried.
Two other scenarios, with their counts. An invoice carrying an obvious prompt injection mostly failed on the model itself: the agent tried to pay the attacker’s account in 1 of 10 runs, and the vault denied it. A swarm-onboarding note, asking payers to run a script and put the output of env in the memo, was ignored in all 5 runs. These are small samples of one model on synthetic inputs, and they cannot establish a rate. The guard’s job is the case where the model does go along, and the fake invoice is that case.
One note on versions. The lab runs used the contract version before the storage fix described below, and the screen before a later memo fix. Neither fix changes what those runs exercised, and the repository’s README says so.
The deployment you can inspect
The public deployment is vault.aunova-agent-guard.testnet, and it is locked. After a final owner withdraw (transaction 8pbdbAAuQMrusVkkQRfVAu8wEU7v1b7YxYAmbnkxXPHt), every access key on the vault was deleted (transaction 2VkLGEm95LvzchWPtBsdsPA9man42Zj9V2a4ZA7tL2pt). It has 0 access keys, so nobody can sign a transaction as the vault any more.
Its ledger holds four entries. Entries 0 and 2 are approved: 0.5 NEAR to the real vendor. Entries 1 and 3 are denied: 5 NEAR to the lookalike, with the reasons receiver_not_allowed, over_per_tx_max and over_window_limit. Entries 0 and 1 come from the scripted official run, and entries 2 and 3 from one real DeepSeek agent run.
The ledger viewer, read live from the chain after the vault was locked. Locking deleted the vault’s own keys; the agent’s policy is still active, so the vault keeps enforcing it.
The log and the chain have to agree. Change one digit in the log and the event id no longer matches its content. Cut the log after the first spend and the hash chain is still valid, but guard verify --complete finds a ledger entry that no log result covers and fails.
Trying to break the record: an edited log and a truncated log, both caught.
Built and broken by an agent team
near-agent-guard was built in the hackathon window by Aunova’s agent team, the same Cowork:Alice setup that ships our Android apps. Alice, the orchestrator, planned and integrated the work. Linus, the code lead, and Ferris, the Rust specialist, built the contract and the guard bridge. Vera, compliance, set the boundaries: testnet only, a spend cap on model calls, and no agent posts anything public. WasmBox is the pre-hackathon baseline, used unmodified as libraries; everything else was written in the window.
Karl, the adversarial reviewer, attacked it. His first review was a REJECT with four blockers. The main one was a storage-griefing bug: every denied request stored a ledger entry paid for by the vault, so an agent key could drain the vault’s storage balance just by collecting denials. A guard that records denials had made denials the attack.
The fix has two parts. Revoked agents are refused outright and store nothing. And a per-window cap on ledger entries bounds the storage: 50 entries at a worst case of 669 bytes each comes to 0.3345 NEAR per window. Karl re-checked and passed the exact deployed bytes before the vault was locked.
The tests behind it: 62 contract unit tests and 11 sandbox tests, 22 spend-screen tests, and 47 unit, 6 CLI and 7 screen tests in the bridge.
Honest limits
This runs on testnet only and has not been audited for mainnet funds.
A prompt-injected agent can still spend inside its policy. If the attacker’s payee is on the allowlist, or the amount is under the caps, the payment goes through. The guard bounds the loss; it does not judge intent.
The window is fixed, not sliding. An agent that spends the full window limit just before a window ends can spend it again just after, so up to twice the window limit can move across one boundary.
If the machine running the bridge is compromised, the contract still bounds spending and every spend still lands on-chain, but only if the owner’s full-access keys live somewhere else. In this demo they do not: for convenience, the setup script stores the owner’s and the agent account’s full-access keys on the same machine as the bridge. A real deployment keeps them on a hardware wallet or behind a multisig. The owner, who can raise the limits, is trusted.
guard verify prints the signer’s npub, but it does not pin it. Our published logs are signed by npub1u9w7vddpzpehc98a4madgfnm643agnr3u6lvxjplm7gmge8l480q2v2j7u; compare that line yourself.
The pinned Wasmtime 42 runtime under WasmBox has known advisories. By our reading none is reachable here, because the only guest is our own hash-checked screen with stdin and stdout only; the README sets out the reasoning so you can check it.
Check it yourself, no keys needed
You do not have to take our word for the ledger. With the prerequisites listed in the README, clone the repository and build the bridge. The first release build takes about 5 to 6 minutes on a laptop, since it compiles Wasmtime.
git clone https://github.com/aunova/near-agent-guard
cd near-agent-guard
(cd bridge && cargo build --release)
export GUARD_CONTRACT_ID=vault.aunova-agent-guard.testnet AGENT_ACCOUNT_ID=agent.aunova-agent-guard.testnet
bridge/target/release/guard ledger
bridge/target/release/guard verify --complete --log demo/audit.jsonl --log demo/agent-take.jsonl
You should see the four on-chain entries and an OK. The two published logs are demo/audit.jsonl, the scripted official run, and demo/agent-take.jsonl, the real DeepSeek run. If you would rather start offline, level 1 in the README runs the contract, screen and bridge test suites with no keys and no testnet.
Links
- near-agent-guard on GitHub: the code, the published logs, and the README with all four ways to try it.
- WasmBox: the sandboxed WebAssembly tool launcher that runs the spend screen.
- The demo on Nostr: the video post.
- The locked testnet vault:
vault.aunova-agent-guard.testnet, and the keyless check that reads its ledger. - NEAR HACKATHON: the event we built it for.
- How we ship Android micro-apps with Rust and an agent team: the previous post, on how Cowork:Alice works.
Aunova builds software that combines AI, Blockchain, Data Analytics and Encryption where the problem needs them. To talk about a project, reach out through aunova.net.