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.

Scroll to dive in

~00

Checks per payment one transaction

authorizePayment()enforced onchain
Mandatewho and when
Mandate existsowner ≠ 0
Policy versionv1
Not revokedrevoke() stops new auths
Not expirednow < expiry
Requestwhat and where
Supported tokenone per deployment
Quote not expirednow ≤ quoteExpiry
Delivery deadline aheaddeadline > now
Approved recipientallowlist
Permitted service idkeccak(service)
LimitsUTC calendar windows
Amount above zero> 0
Per-transaction limithard cap
Daily limitresets 00:00 UTC
Monthly limitresets on the 1st
Fundsbalance − reserved
Available fundsescrow counted
Replayonce, ever
Unique request idbytes32
Unique nonceper mandate
Approvalabove the threshold
Owner approval attachedEIP-712, one request

03 — Every decision already made

Every rule, in order. So the agent can get to the actual work.

  1. 001 / Transaction limit

    A hard cap on any single payment. Approval cannot raise it; a bigger job needs a new mandate.

  2. 002 / Daily and monthly budgets

    UTC calendar day and UTC calendar month. Counted when a payment is authorized, so settlement never counts twice.

  3. 003 / Approved recipients

    Payments go only to addresses on the list. An unknown address is refused before any funds move.

  4. 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.

  5. 005 / Expiry

    After the timestamp, no new authorizations. Escrow already open keeps its own settlement and dispute terms.

  6. 006 / Approval threshold

    Above it, the owner signs a message bound to that exact request: amount, recipient, service, job, nonce, chain and contract.

  7. 007 / Revocation

    One call stops everything new. Uncommitted funds stay withdrawable; pending approvals become useless.

  8. 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.

  1. 01PASSMandate existsMandate #7 owned by 0x0000…0001.
  2. 02PASSPolicy versionRequest v1, mandate v1.
  3. 03PASSNot revokedMandate is active.
  4. 04PASSNot expiredExpires at 1796126400 (now 1793534400).
  5. 05PASSSupported tokenRequest token 0x5fc5…d168; deployment token 0x5fc5…d168.
  6. 06PASSQuote not expiredQuote valid until 1793535000.
  7. 07PASSDelivery deadline in the futureDeadline 1793538000.
  8. 08PASSApproved recipient0xA100…00A1 is on the allowlist (2).
  9. 09PASSPermitted serviceService id 0x0ce5…62b6 is permitted.
  10. 10PASSAmount above zeroAmount 18000000.
  11. 11PASSPer-transaction limitAmount 18000000 vs limit 25000000.
  12. 12PASSUTC daily limitSpent today 60000000 + 18000000 vs 100000000.
  13. 13PASSUTC monthly limitSpent this month 410000000 + 18000000 vs 1000000000.
  14. 14PASSAvailable fundsAvailable 160000000 (balance 180000000 − reserved 20000000).
  15. 15PASSUnique request idRequest id unused.
  16. 16PASSUnique nonceNonce unused.
  17. 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

REQUESTRULE CHECKSAPPROVALESCROWSETTLEMENT

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

REQUESTRULE CHECKSBLOCKEDAPPROVALESCROWSETTLEMENT

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.

SolidityTypeScript
This is the actual contract.

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.

OpenConsole

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.

Dispute authorityconfigured
Disputes are decided by an explicitly configured resolver set at deployment. It can split a disputed amount between provider and mandate and nothing else. If it does not decide within the resolution timeout, anyone can refund the owner. A configured arbiter, not trustless adjudication. Challenge period on this deployment: 3 days.
  1. 001 / Reserve

    authorizePayment() checks every rule, consumes the daily and monthly allowance, and reserves the amount in escrow. The agent gets a request id.

  2. 002 / Deliver

    The provider does the work and calls submitDelivery(receiptHash) before the delivery deadline. The challenge period starts.

  3. 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.

  4. 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.

  5. 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×

  1. 01Create, fund, revoke — immutable policy, replace to change
  2. 02Approve above-threshold requests with one EIP-712 signature
  3. 03Accept, dispute, or let the challenge period settle
  4. 04Withdraw uncommitted funds at any time
  5. 05Private labels, approvals and activity; public chain data stays public
OpenConsole

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

  1. 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.

Give your agents room to work.
Keep the boundaries yours.