01 — the spending control layer
enforced onchain
02 — Define the boundaries
Six rules. One contract. Every payment, in order.
This is the part a prompt cannot do. Every rule is a field in the onchain mandate and every authorization walks all of them before a single unit is reserved.
Spending controls for AI agents · Robinhood Chain.
Agent freedom.
Your boundaries.
Set budgets, define permissions, and review how your agents spend.
Hestr gives an agent a defined spending mandate: a budget, approved recipients, permitted services, and an expiry. The limits live in a contract, not in a prompt. Agents request payments inside them; you approve exceptions and revoke whenever you like.
~00
Checks per payment one transaction
03 — Every decision already made
Every rule, in order. So the agent can get to the actual work.
001 / Transaction limit
A hard cap on any single payment. Approval cannot raise it; a bigger job needs a new mandate.
002 / Daily and monthly budgets
UTC calendar day and UTC calendar month. Counted when a payment is authorized, so settlement never counts twice.
003 / Approved recipients
Payments go only to addresses on the list. An unknown address is refused before any funds move.
004 / Permitted service ids
Hashed identifiers such as text.transform. They limit what can be paid for; they do not prove a provider performs the service.
005 / Expiry
After the timestamp, no new authorizations. Escrow already open keeps its own settlement and dispute terms.
006 / Approval threshold
Above it, the owner signs a message bound to that exact request: amount, recipient, service, job, nonce, chain and contract.
007 / Revocation
One call stops everything new. Uncommitted funds stay withdrawable; pending approvals become useless.
008 / Escrow, not transfer
Authorization reserves funds inside the mandate. They can only reach the quoted provider or return to the mandate.
character field · sampled from the mark · the mark itself stays in the hero
04 — Follow one payment
Move the amount. Drop the approval. Watch the order of checks.
This walkthrough runs the production policy evaluator against a sample mandate, so what you see is the real order of checks. Illustration only: example amounts, not live activity.
Illustration · sample mandate #7 · example amounts
- per tx
- 25 tUSD
- today
- 60 tUSD / 100 tUSD
- month
- 410 tUSD / 1,000 tUSD
- available
- 160 tUSD
- approval above
- 10 tUSD
Allowed
Allowed by policy.
- 01PASSMandate existsMandate #7 owned by 0x0000…0001.
- 02PASSPolicy versionRequest v1, mandate v1.
- 03PASSNot revokedMandate is active.
- 04PASSNot expiredExpires at 1796126400 (now 1793534400).
- 05PASSSupported tokenRequest token 0x5fc5…d168; deployment token 0x5fc5…d168.
- 06PASSQuote not expiredQuote valid until 1793535000.
- 07PASSDelivery deadline in the futureDeadline 1793538000.
- 08PASSApproved recipient0xA100…00A1 is on the allowlist (2).
- 09PASSPermitted serviceService id 0x0ce5…62b6 is permitted.
- 10PASSAmount above zeroAmount 18000000.
- 11PASSPer-transaction limitAmount 18000000 vs limit 25000000.
- 12PASSUTC daily limitSpent today 60000000 + 18000000 vs 100000000.
- 13PASSUTC monthly limitSpent this month 410000000 + 18000000 vs 1000000000.
- 14PASSAvailable fundsAvailable 160000000 (balance 180000000 − reserved 20000000).
- 15PASSUnique request idRequest id unused.
- 16PASSUnique nonceNonce unused.
- 17PASSOwner approval attachedAmount 18000000 exceeds threshold 10000000; approval attached.
The same checks run in the contract’s previewAuthorization() and again inside authorizePayment(). Evaluation is advisory: concurrent spending can change the answer before the transaction is mined.
Illustration · example amounts · not live activity
Allowed · 18.00 tUSD to Transcription Co. · text.transform
Above the approval threshold, so the owner signs; funds are reserved; the provider delivers; the owner accepts or the challenge period elapses.
Blocked · 12.00 tUSD to an unknown address · text.transform
Blocked at Rule checks: recipient not on the mandate’s allowlist. Nothing is reserved; the agent gets the reason.
05 — The contract
This is the actual contract.
Solidity 0.8.26, OpenZeppelin 5.7, 58 Foundry tests with invariants and reentrancy. Tested, not audited. One token per deployment, immutable mandates, explicit resolver.
- ▸packages/contracts/src
- ·HestrMandates.sol
- ·HestrTypes.sol
- ·UtcTime.sol
- ·test/MockUSD.sol
- ▸packages/contracts/test
- ·HestrMandates.t.sol
- ·Invariants.t.sol
- ·UtcTime.t.sol
- ▸packages/protocol/src
- ·policy.ts
- ·eip712.ts
- ▸packages/sdk/src
- ·client.ts
- ▸packages/mcp/src
- ·server.ts
- ▸services/indexer
- ▸examples/provider
HestrMandates.sol — _checkPolicy()
1function _checkPolicy(Mandate storage m, PaymentRequest calldata req, bool approvalProvided)2 internal view returns (ReasonCode)3{4 if (m.owner == address(0)) return ReasonCode.MandateNotFound;5 if (req.policyVersion != m.policyVersion) return ReasonCode.PolicyVersionMismatch;6 if (m.revoked) return ReasonCode.MandateRevoked;7 if (block.timestamp >= m.expiry) return ReasonCode.MandateExpired;8 if (req.token != address(token)) return ReasonCode.UnsupportedToken;9 if (block.timestamp > req.quoteExpiry) return ReasonCode.QuoteExpired;10 if (req.deliveryDeadline <= block.timestamp) return ReasonCode.InvalidDeliveryDeadline;11 if (!recipientAllowed[req.mandateId][req.recipient]) return ReasonCode.RecipientNotAllowed;12 if (!serviceAllowed[req.mandateId][req.serviceId]) return ReasonCode.ServiceNotAllowed;13 if (req.amount == 0) return ReasonCode.ZeroAmount;14 if (req.amount > m.txLimit) return ReasonCode.ExceedsTransactionLimit;15 if (spentOnDay[req.mandateId][UtcTime.dayIndex(block.timestamp)] + req.amount > m.dailyLimit)16 return ReasonCode.ExceedsDailyLimit;17 if (spentInMonth[req.mandateId][UtcTime.monthIndex(block.timestamp)] + req.amount > m.monthlyLimit)18 return ReasonCode.ExceedsMonthlyLimit;19 if (m.balance - m.reserved < req.amount) return ReasonCode.InsufficientAvailableBalance;20 if (req.requestId == bytes32(0)) return ReasonCode.RequestIdUsed;21 if (_payments[req.requestId].state != PaymentState.None) return ReasonCode.RequestIdUsed;22 if (requestNonceUsed[req.mandateId][req.nonce]) return ReasonCode.NonceUsed;23 if (req.amount > m.approvalThreshold && !approvalProvided) return ReasonCode.ApprovalRequired;24 return ReasonCode.Ok;25}
TERMINAL
~/hestr > pnpm dev # anvil → deploy → postgres → web + indexer + provider
~/hestr > pnpm demo # 402 → policy → escrow → delivery
mode: unconfigured · contract: not configured · tested, not audited
06 — Your agents, in view
The console. Balances from the chain, decisions from you.
Mandates with live available and reserved balances, the approval queue, every payment with its onchain state and receipt, and an activity log you can filter and export. The screens below are real captures from the local development stack (this deployment has no network configured); hover to see them.

Overview
Live balances, reserved escrow, pending approvals

Create a mandate
Readable summary before you sign

Mandate detail
Limits, allowlists, fund / withdraw / revoke

Approval queue
Every rule check, one EIP-712 signature

Payment — delivered
Receipt hash, challenge period, accept or dispute

Payment — settled
Terminal state with the settlement transaction

Payments
Offchain workflow states next to onchain states

Activity log
Who, when, amount, result, transaction; CSV export
07 — How settlement works
Reserve, deliver, settle. Lifetime of one escrow.
Escrow is real: the amount leaves the available balance the moment it is authorized and can only go to the quoted provider or back to the mandate.
001 / Reserve
authorizePayment() checks every rule, consumes the daily and monthly allowance, and reserves the amount in escrow. The agent gets a request id.
002 / Deliver
The provider does the work and calls submitDelivery(receiptHash) before the delivery deadline. The challenge period starts.
003 / Accept or wait
The owner can accept and settle immediately. Otherwise, after the challenge period, anyone can settle an undisputed delivery to the provider.
004 / Dispute
Inside the challenge period the owner can dispute. Funds stay locked until the resolver decides how much goes to the provider; the rest returns to the mandate.
005 / Refund
No delivery by the deadline: anyone can release the escrow back to the mandate. Refunds restore available funds but never restore consumed period allowance.
A receipt hash shows the provider recorded a delivery. It does not prove the work was correct. The challenge period exists so the owner can check.
08 — Two ways in
Owners sign. Agents request. One enforcement path.
SDK, HTTP API, MCP tools and the example provider all go through an agent-signed EIP-712 request, an owner-signed approval when required, and the contract.
Rules an agent can bypass
0×
- 01Create, fund, revoke — immutable policy, replace to change
- 02Approve above-threshold requests with one EIP-712 signature
- 03Accept, dispute, or let the challenge period settle
- 04Withdraw uncommitted funds at any time
- 05Private labels, approvals and activity; public chain data stays public
TypeScript SDK · @hestr/sdk
Read mandates and budgets, evaluate a proposal, request approval, submit an authorized payment, read receipts — or fetch a paid URL and let the client handle the 402.
const hestr = new HestrClient({ apiUrl, rpcUrl, chainId, contract, agentPrivateKey })
const { response, receipt } = await hestr.fetchWithEscrow(url, { method: 'POST', body })MCP tools · @hestr/mcp
hestr_get_budget, hestr_evaluate_request, hestr_request_payment, hestr_get_payment, hestr_fetch_paid_resource. Approvals still come from the owner.
"hestr": { "command": "pnpm", "args": ["--filter", "@hestr/mcp", "start"],
"env": { "HESTR_AGENT_PRIVATE_KEY": "<agent key, never the owner>" } }Example provider · hestr-escrow/1
A deterministic paid text service that quotes with HTTP 402, verifies the escrow onchain, delivers and records the receipt hash itself. Hestr’s own scheme — not x402.
402 Hestr-Payment-Required: <quote> POST Hestr-Payment: <requestId, txHash> 200 Hestr-Payment-Receipt: <receiptHash, deliveryTx>
x402 status: not available. The verified x402 schemes on Robinhood Chain settle by direct transfer and cannot express escrow.
09 — Before you integrate
Before you integrate
- The HestrMandates contract. authorizePayment() checks the mandate (active, unexpired), the token, the quote expiry, the delivery deadline, the recipient and service allowlists, the per-transaction cap, the UTC daily and monthly caps, available funds, request-id and nonce uniqueness, and the owner approval when the amount is above the threshold. The console, the API, the SDK and the MCP tools only explain, queue and relay.
08 — Token
$HESTR
The Hestr token on Robinhood Chain. It is separate from the protocol: mandates pay providers in the deployment's payment token (USDG on Robinhood Chain), and no rule in the contract depends on $HESTR.
Follow @HestrAgent- Ticker
- $HESTR
- Network
- Robinhood Chain (4663)
- Contract address
- Soon
- Status
- not launched · no address exists yet
- Official channels
- This section · @HestrAgent
The only official contract address will be published here and on @HestrAgent. Any other address, airdrop, presale or “claim” is not Hestr.
