Spending limits for agents

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_approval and issue_subrein.
  • Revoke kills it. Freeze, revoke a rein and its subtree, or revoke every rein you own in one transaction.
you.testnet (full-access key: deposits, approvals, revokes) ├── vault in reins-near.testnet Ⓝ1.7 └── rein #2 Ⓝ0.5 / day · Ⓝ0.2 per payment · ask above Ⓝ0.1 ← function-call key on you.testnet └── sub-rein #3 Ⓝ0.1 / day ← issued by the agent, key added by you
Contract
reins-near.testnet
Network
NEAR testnet

How a Payment Works

  1. 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.
  2. Dry run. preview_pay is a free view that runs the exact same check as pay.
  3. Pay. The agent signs pay with 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.
  4. Settle. A callback confirms the transfer. If it failed (say, the account does not exist), budget, uses and vault are restored and a payment_failed event 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

TermMeaningChecked
budgetMax spend per window (or lifetime when period_secs is 0)every rein up the chain
period_secsWindow length; the window resets after it elapsesevery rein up the chain
per_tx_capLargest single payment, approval or notevery rein up the chain
approval_aboveAbove this the agent must file a requestthe paying rein
expires_atUnix seconds after which the rein stopsevery rein up the chain
max_usesPayments over the rein's lifeevery rein up the chain
merchantsIf set, the only accounts the rein may payevery rein up the chain
blocklistAccounts with a bonded report, unless you allowed themevery payment
vaultYour balance must cover the paymentevery 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.

CodeMeaning
unknown_keyThe signing key is not bound to a rein (never was, or revoked)
signer_must_be_ownerThe call was relayed through another contract
rein_not_activeThis rein or an ancestor is frozen or revoked
rein_expiredPast expires_at
uses_exhaustedmax_uses reached
merchant_not_allowedReceiver not in the merchant lock
over_per_tx_capAbove the per-payment cap
over_budgetNot enough left in this window, here or up the chain
receiver_blockedReceiver is on the shared blocklist for this owner
vault_insufficientThe vault cannot cover it
needs_approvalAbove approval_above · use request_approval
self_paymentPaying the owner or the contract itself
zero_amountAmount 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

ToolWhat it does
rein_statusTerms, what is left this window, the vault, pending approvals · call it first
check_counterpartyallow / warn / block with reasons for a receiver
preview_paymentThe free preview_pay dry run
payGuarded payment: refuses on block, asks you on warn, files a request above the threshold
approval_statusState of a request the agent filed
issue_subreinA narrower rein for a sub-agent's public key
quote_cross_chainNEAR Intents 1Click dry quote · never moves funds

Function-Call Key Limits

  • It can call only pay, request_approval and issue_subrein on 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

SignalSourceLevel
Shared blocklistReins reports_of + is_blockedblock
Named account does not existRPC view_accountblock
Look-alike of a known merchantedit distance over a homoglyph skeletonblock
Account younger than 7 daysNEARBlocks account created timewarn (info if allowlisted)
Contract code changed in 7 daysarchival RPC view_account, then and nowwarn
First payment to this accountReins pair_countwarn (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

MethodWhoNotes
deposit / withdrawownerattach NEAR / 1 yocto
create_rein / create_subreinownerattach storage (about 0.02 NEAR)
update_terms · freeze · unfreeze · revoke · revoke_allowner1 yocto
approve_request · reject_request · set_allowowner1 yocto
pay · pay_evm · request_approval · issue_subreinagent keyno deposit
withdraw_tokenowner1 yocto
ft_on_transfertoken contractcredits your token vault
report · retract_reportanyone0.1 NEAR bond · 1 yocto
preview_pay · get_rein · reins_of · rein_for_key · vault_ofviewfree
pending_requests_of · reports_of · is_blocked · pair_count · statsviewfree

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.