Documentation
Reins gives an AI agent a NEAR function-call key instead of your keys. The Reins contract holds the rules and checks every payment; the agent can spend inside them and nothing else. Here is how it works, end to end.
Overview
Agents need to spend money. Handing one a full-access key is reckless; funding a separate wallet gives up your limits. Reins uses what NEAR already has: an account can hold function-call access keys that may call only named methods of one contract and can never attach a deposit.
- The vault is the account. You deposit NEAR into the Reins contract; it is held for you and only your full-access key can withdraw it.
- A rein is terms bound to a key. Budget per window, per-payment cap, approval threshold, merchant lock, expiry, usage count — stored on-chain against one public key.
- The agent holds a function-call key. It lives on your account but can call only
pay,request_approvalandissue_subrein. - Revoke kills it. Freeze, revoke a rein and its subtree, or revoke every rein you own in one transaction.
How a Payment Works
- Check the receiver. The MCP server scores it: shared blocklist, existence, age, recent contract code changes, your history with it, look-alike names. block refuses to sign; warn asks you.
- Dry run.
preview_payis a free view that runs the exact same check aspay. - Pay. The agent signs
paywith its key. The contract finds the rein from the signing key, checks every rule along the rein and its ancestors, books the spend, and transfers from your vault. - Settle. A callback confirms the transfer. If it failed (say, the account does not exist), budget, uses and vault are restored and a
payment_failedevent is emitted.
Creating a Rein
In the console, press + on the deck and describe the rein, or set the terms yourself. Your wallet signs two transactions:
# 1. bind a fresh key to the terms (deposit covers storage; the rest stays in the vault)
create_rein { agent_key, label, terms } attached: 0.02 NEAR
# 2. add that key to your account, for the agent methods only
AddKey ed25519:… FunctionCall { receiver: reins-near.testnet, methods: [pay, request_approval, issue_subrein], allowance: 0.25 NEAR }The agent's secret key is generated in your browser and shown once. Reins never stores it.
Rein Terms
| Term | Meaning | Checked |
|---|---|---|
budget | Max spend per window (or lifetime when period_secs is 0) | every rein up the chain |
period_secs | Window length; the window resets after it elapses | every rein up the chain |
per_tx_cap | Largest single payment, approval or not | every rein up the chain |
approval_above | Above this the agent must file a request | the paying rein |
expires_at | Unix seconds after which the rein stops | every rein up the chain |
max_uses | Payments over the rein's life | every rein up the chain |
merchants | If set, the only accounts the rein may pay | every rein up the chain |
| blocklist | Accounts with a bonded report, unless you allowed them | every payment |
| vault | Your balance must cover the payment | every payment |
Error Codes
pay panics with exactly one of these, and preview_pay returns the same code without moving anything, so an agent can relay the reason instead of guessing.
| Code | Meaning |
|---|---|
unknown_key | The signing key is not bound to a rein (never was, or revoked) |
signer_must_be_owner | The call was relayed through another contract |
rein_not_active | This rein or an ancestor is frozen or revoked |
rein_expired | Past expires_at |
uses_exhausted | max_uses reached |
merchant_not_allowed | Receiver not in the merchant lock |
over_per_tx_cap | Above the per-payment cap |
over_budget | Not enough left in this window, here or up the chain |
receiver_blocked | Receiver is on the shared blocklist for this owner |
vault_insufficient | The vault cannot cover it |
needs_approval | Above approval_above · use request_approval |
self_payment | Paying the owner or the contract itself |
zero_amount | Amount is zero |
Connecting an Agent
The Reins MCP server runs next to the agent and signs with the rein's key. The connect page fills in the command for you.
claude mcp add reins \ -e REINS_OWNER=you.testnet \ -e REINS_AGENT_KEY=ed25519:<agent-secret> \ -- bun run <path-to-Reins>/packages/mcp/src/index.ts
The server asks you through MCP elicitation when the counterparty check says warn. A client without elicitation gets a refusal and a message to ask you instead.
MCP Tools
| Tool | What it does |
|---|---|
rein_status | Terms, what is left this window, the vault, pending approvals · call it first |
check_counterparty | allow / warn / block with reasons for a receiver |
preview_payment | The free preview_pay dry run |
pay | Guarded payment: refuses on block, asks you on warn, files a request above the threshold |
approval_status | State of a request the agent filed |
issue_subrein | A narrower rein for a sub-agent's public key |
quote_cross_chain | NEAR Intents 1Click dry quote · never moves funds |
Function-Call Key Limits
- It can call only
pay,request_approvalandissue_subreinon the Reins contract. - It cannot attach a deposit. Every owner method requires at least 1 yoctoNEAR attached, so the key can never withdraw, approve, unfreeze, change terms or override the blocklist — even if it were granted those names.
- It cannot transfer NEAR, add keys or deploy code: NEAR refuses those for a function-call key.
- It spends gas from its allowance (0.25 NEAR by default). When the allowance runs out, the key stops signing; Settings shows what is left on each key.
Revoking a rein makes the key unknown to the contract. Also delete it from your account so it stops consuming allowance; the console does both.
Owner Approvals
A payment above approval_above (and within every other rule) becomes a request instead. You approve it in Settings with your wallet; the contract checks every other rule again and pays. Requests expire after 24 hours, and a rein can hold at most 4 open ones.
Sub-Reins & Revocation
An agent can split its allowance with issue_subrein: the child's budget and cap may only be narrower, depth is at most 3 and a rein has at most 8 children. Spending by a sub-rein counts against every ancestor. The sub-agent's key only works once you add it to your account, so an agent can split its allowance but never mint a new one.
Revoking a rein revokes its whole subtree; revoke_all revokes every rein you own.
Counterparty Guard
| Signal | Source | Level |
|---|---|---|
| Shared blocklist | Reins reports_of + is_blocked | block |
| Named account does not exist | RPC view_account | block |
| Look-alike of a known merchant | edit distance over a homoglyph skeleton | block |
| Account younger than 7 days | NEARBlocks account created time | warn (info if allowlisted) |
| Contract code changed in 7 days | archival RPC view_account, then and now | warn |
| First payment to this account | Reins pair_count | warn (info if allowlisted) |
A signal that cannot be read is reported as unavailable and never counts as risk or as safety.
Anyone can report an account with a 0.1 NEAR bond and the sha256 of the evidence. Every owner's reins refuse it unless that owner calls set_allow. See the Guard console.
Tokens, Chains & Accounts
A rein's terms pick what it spends. With no extra field it pays NEAR from your vault. With token set to a NEP-141 contract (tUSD on the demo) it pays that token with ft_transfer from your token vault; fund it with the token's ft_transfer_call to the Reins contract. With evm_chain_id set (84532, Base Sepolia) the agent calls pay_evm: the contract checks the rein and asks NEAR's Chain Signatures network (v1.signer-prod.testnet) to sign the EVM payment from an address derived for that rein. Amounts are in the currency's smallest unit.
Pass agent_account to create_rein to put the agent's key on a named sub-account such as research.you.testnet instead of your own account. The Connect wizard creates the sub-account, funds its storage and adds the key in the same wallet approval.
Cross-Chain Quotes
quote_cross_chain asks the NEAR Intents 1Click API for a dry: true quote: what paying a merchant on another chain would cost from NEAR. No deposit address is issued and nothing moves. Intents has no testnet, so quotes are mainnet prices.
Contract Methods
| Method | Who | Notes |
|---|---|---|
deposit / withdraw | owner | attach NEAR / 1 yocto |
create_rein / create_subrein | owner | attach storage (about 0.02 NEAR) |
update_terms · freeze · unfreeze · revoke · revoke_all | owner | 1 yocto |
approve_request · reject_request · set_allow | owner | 1 yocto |
pay · pay_evm · request_approval · issue_subrein | agent key | no deposit |
withdraw_token | owner | 1 yocto |
ft_on_transfer | token contract | credits your token vault |
report · retract_report | anyone | 0.1 NEAR bond · 1 yocto |
preview_pay · get_rein · reins_of · rein_for_key · vault_of | view | free |
pending_requests_of · reports_of · is_blocked · pair_count · stats | view | free |
Events
Every state change logs one NEP-297 line, EVENT_JSON:{"standard":"reins",…}: rein_created, payment, payment_failed, approval_requested, approved, rejected, frozen, unfrozen, revoked, terms_updated, deposit, withdraw, reported, report_retracted, allow_set. The contract keeps no payment history in state; the events are the history.
SDK & CLI
import { ReinsClient, AgentClient, checkCounterparty } from "@reins/sdk";
const reins = new ReinsClient("reins-near.testnet");
await reins.previewPay(owner, agentPublicKey, "shop.testnet", amountYocto);
const agent = new AgentClient(owner, agentSecret, provider, "reins-near.testnet");
await agent.guardedPay({ receiver: "shop.testnet", amountNear: "0.05", askHuman });bun run reins status # vault, reins, pending approvals bun run reins approve 3 # approve request 3 with your full-access key bun run reins panic # revoke every rein
Verify It Yourself
cargo test -p reins # contract unit tests bun test packages # SDK and MCP tests bun run verify # keyless: re-reads every recorded tx from the chain
Ready? Open the console — the demo owner's reins are readable without signing in.