Live and verified on Base Sepolia and Whitechain Sepolia

A wallet your agent can run and cannot drain.

Rein is a smart account. You own it. Your agent holds a key that can only make the calls your policy admits, and the policy is enforced by the chain, not by the agent. A fully hijacked agent still cannot move a coin the policy forbids.

MIT. Standard Solidity, no PUSH0, deploys wherever your agent's money already is. Source.

npm run demo:base · Base Sepolia, chain 84532

    

Giving an agent a wallet is all-or-nothing today.

Either the agent holds a key, and one bad instruction empties the account. Or it does not, and a human is back in the loop for every payment, which is what the agent was meant to remove.

option one

Give it the key

The agent's own code is the only boundary. Code that reads untrusted PDFs, tool results and web pages, and can be talked into anything by any of them.

option two

Approve every call

Safe, and pointless. The agent cannot pay a supplier at 3am, so you are still the treasury, with an extra step.

the usual patch

A session key with a cap

Better, and not enough. A flat cap does not know what the agent is doing, and the shapes real theft takes are exactly the ones a cap cannot see.

  • "Pay this supplier and nobody else." A cap has no idea who the money went to.
  • "Never sign an unlimited allowance." A cap on transfers says nothing about approvals.
  • "500 an hour across every call you make, not 500 per call." Per-call limits are drained one call at a time.

Three steps, and the third one is the product.

The owner writes the policy. The agent gets a key. The chain refuses, in words, before any gas is spent.

  1. owner, once

    Write the policy on chain

    Which contracts, which functions, which payees, how much per rolling window, how many calls, what allowance ceiling, when the key expires. Every path that widens a policy is owner-only.

  2. agent, every call

    Ask before acting

    simulate() is a free view call that returns the same code execute() would revert with. A well-built agent checks whether an action is covered and abstains when it is not, instead of finding the boundary by hitting it. The test suite fails if the two paths ever disagree.

  3. chain, always

    Refuse, legibly, with no way around

    An agent can never call its own account, so no sequence of permitted calls ends in more permission, including one routed through an allowlisted contract that calls back. A guardian key can stop the agent and can never spend. Every call carries a hash of the instruction behind it.

What the chain enforces, exactly.

Policy is written over what a call does, not only over its selector. An allowlisted transfer still has to clear a rolling spend window and a payee allowlist, so "can call transfer" is never "can drain the account".

target allowlist
which contracts, per agent key
selector allowlist
which functions on each of them
payee allowlist
who may receive tokens. The agent cannot invent a recipient.
rolling spend window
native and per token, cumulative across every call in the window, not per call
call rate
how many calls per window
approval ceiling
approve() is capped at an absolute figure. increaseAllowance() is refused outright, because a capped delta bounds nothing.
expiry
agent keys lapse on their own
guardian breaker
a key that can stop the agent and can never spend. A monitoring bot can hold one safely.
intent trail
every call carries a hash of the instruction behind it, so your prompt logs join to the chain
no self-call
target == address(this) is refused. There is no sequence of permitted calls that ends in more permission.

Sixteen reasons to say no, one table

simulate() returns one of these and execute() reverts with the same value. An agent that asks first and one that just tries get an identical answer, which is what makes asking first trustworthy.

Deterministic addresses

The factory computes an account's address before it exists, so you can show it to a user, or fund it from an exchange withdrawal, and deploy afterwards.

0
OKthe call is inside policy
1
NOT_AN_AGENTno policy for this key
2
AGENT_EXPIREDthe key lapsed
3
BREAKER_TRIPPEDa guardian stopped this agent
4
SELF_CALLan agent may not call its own account
5
TARGET_NOT_ALLOWEDthat contract is not on the list
6
SELECTOR_NOT_ALLOWEDthat function is not on the list
7
NATIVE_PER_CALLtoo much value in one call
8
NATIVE_PER_WINDOWtoo much value this window
9
CALL_RATEtoo many calls this window
10
TOKEN_NOT_ALLOWEDthat token has no policy
11
TOKEN_PER_WINDOWmore of this token than remains this window
12
APPROVAL_TOO_LARGEan allowance above the ceiling
13
PAYEE_NOT_ALLOWEDthat recipient is not on the list
14
INTENT_REQUIREDno record of the instruction behind the call
15
DELTA_APPROVAL_UNSUPPORTEDincreaseAllowance is refused outright

A fully compromised agent tried six ways to empty the account. Loss: zero.

Unedited output of the demo script, as it ran on chain. An agent is hired to pay suppliers, does its job, then reads an injected instruction and tries in earnest to empty the account. Every refusal below is a free view call that returns the same code the transaction would revert with. The same bytecode ran the same way on all three chains.

npm run demo:whitechain · Whitechain Sepolia, chain 1874

    
Verified deployments
Base Sepolia (84532)Whitechain Sepolia (1874)GOAT Testnet3 (48816)
demo account, verified source0x0dA3…05270x69a5…46210xaF04…C866
factory, verified source0xa1B4…31910x30F0…A3160x9b6B…Df90
the one payment allowed0xfc20…7b510x334a…afdf0xb584…60b4
the rating that carries it——0x03bf…7fce
everything after itrefusedrefusedrefused

On GOAT: a rating that cannot exist without the payment

Measured ERC-8004 deployments show 98.7 to 100 percent of feedback records carry no proof of payment, and most reviewers are Sybil-coordinated. So on GOAT the account's policy admits exactly two things: pay allowlisted sellers within the budget, and call giveFeedback. The feedback carries a receipt naming the payment transaction, the intent hash and the policy in force, and feedbackHash is its keccak. Decode it, hash it, find the settlement on chain. Feedback the account never paid for cannot be produced.

It ran on Testnet3: the payment, then the rating that names it, then five compromised attempts refused and an attacker balance of zero.

One thing we found by running it

On GOAT Testnet3 the Reputation Registry validates agents against identity registry 0x54B8…15ce, while the address table in GOATNetwork/agentkit lists 0x5560…5522. Register on the published one and giveFeedback reverts with ERC721NonexistentToken. Mainnet is consistent. These scripts ask the reputation registry which identity registry it uses instead of trusting the table, so they work on both.

The same run on your own machine, with no chain, no key and nothing to sign up for:

git clone https://github.com/let-the-dreamers-rise/rein && cd rein && npm install
npm test                                    # 56 tests
npx hardhat run scripts/demo-injection.js   # the transcript above, on an in-process chain
Rein v2 · in development · runs end to end today, on a local chain

The policy, compiled from what your agent actually does.

Today the owner writes the policy by hand, like every wallet policy engine. That is the weak point: hand-written policies are either loose enough to drain or tight enough to put a human back in the loop. v2 reads the agent's own intent trail back from the chain, compiles it into sentences with the nyaya synthesiser, and writes the result to a fresh key. One command runs the whole loop and measures it.

compiled from 89 shadow-mode calls, 22 held out · npm run v2each line is a rule with the evidence that earned it
  • pays only Payroll, Supplier A, Supplier B, Supplier C 81 of 81 · payee allowlist
  • never moves more than 3,201 USDT in any hour; ceiling 4,002 with 25% headroom 81 of 81 · spend window
  • never approves more than 500 USDT; on the router, calls only swapExact 4 of 4 · ceilings, allowlists
  • pays Supplier B on Tuesdays; Supplier A receives 100 to 500 in the morning 21 of 22, 21 of 21 · monitor only
  • the guardian watches the habits: 0 of 22 held-out calls flagged; one in-bounds, off-habit payment allowed by the contract, flagged, breaker tripped measured
  • held out: 22 of 22 honest calls allowed. Attacks: 10 of 10 refused, attacker balance 0 measured on chain
  • of those ten, the wide policy already refused 8 before compiling; 2 are attributable to the compiled policy the denominator, stated
  • an in-bounds drain to an allowlisted payee succeeds: 208,000 USDT, 0 guardian flags the honest limit of a compiled bound

Compile

Induce which targets, functions, payees, amounts per window and sequences are normal, and emit a one-page readable policy plus the enforcement artefact: Rein's account, and the same bounds as Turnkey, Coinbase and Privy policy JSON, with a table of what each engine can hold. Shape-checked against their docs, not yet run against their APIs.

Measure

Every compiled policy ships with two numbers and their denominators. Coverage: how often honest actions get blocked on held-out traces. Catch rate: how many injected attacks are refused. A policy that cannot state those numbers is a guess.

Prove

Every allow and refusal carries a receipt. A merchant, treasury owner or insurer can verify the agent was inside an attested policy before accepting settlement. On stablecoins there is no chargeback, so the control has to exist before the money leaves.

The rule synthesiser behind this is nyaya, open source, standard library only, already learning readable rules from real data on a phone.

Ninety seconds.

Silent, captioned. The same on-chain run as above.

What Rein does not do.

Written here so nobody has to find it in the code.

  • The intent hash is a commitment, not a proof. The contract cannot check that the hash matches the instruction the agent actually received. What you get is that the agent signed something before spending, and that your prompt logs join to the chain on that key.
  • A permitted call is still a permitted call. Rein bounds the blast radius. It does not make the agent's decisions good.
  • It assumes the owner is not compromised. Use a multisig for anything that matters.
  • Only transfer, approve, transferFrom and increaseAllowance are decoded. Other value-moving methods on an allowlisted token are outside what the policy understands.
  • Not audited. 56 tests pass. That is not an audit.