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.
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.
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.
Approve every call
Safe, and pointless. The agent cannot pay a supplier at 3am, so you are still the treasury, with an extra step.
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.
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.
agent, every call
Ask before acting
simulate()is a free view call that returns the same codeexecute()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.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.
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.
| Base Sepolia (84532) | Whitechain Sepolia (1874) | GOAT Testnet3 (48816) | |
|---|---|---|---|
| demo account, verified source | 0x0dA3…0527 | 0x69a5…4621 | 0xaF04…C866 |
| factory, verified source | 0xa1B4…3191 | 0x30F0…A316 | 0x9b6B…Df90 |
| the one payment allowed | 0xfc20…7b51 | 0x334a…afdf | 0xb584…60b4 |
| the rating that carries it | — | — | 0x03bf…7fce |
| everything after it | refused | refused | refused |
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
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.
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,transferFromandincreaseAllowanceare 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.