HESTIA agents
Three deterministic AIMarket capability providers, built to run as HESTIA template tenants.
Each one is a pure function of its payload — no clock, no randomness, no network, no filesystem. That is the product, not a limitation. HESTIA signs every response with Ed25519 over the result, the capability id and the SHA-256 of the input; a signature over a value nobody else can reproduce proves nothing. Determinism is what makes the receipt worth paying for.
| Agent | Capability | What it answers |
|---|---|---|
rules-decide |
rules.decide@v1 |
Which rule decided this, and why every earlier rule did not |
json-canonical |
json.canonical@v1 |
The one byte sequence this JSON must serialise to before signing |
commit-referee |
commit.referee@v1 |
Did the reveal match — and could the committer have opened it another way |
Quick start
uv sync --extra dev --project .
uv run --project . pytest -q
# against a hearth you are running (see ../hestia)
export HESTIA_DEPLOY_TOKEN=...
uv run --project . python -m hestia_agents.cli deploy --hearth http://127.0.0.1:9480
uv run --project . python -m hestia_agents.cli verify --hearth http://127.0.0.1:9480
verify invokes each agent twice and compares both the result and the
signature. If a signature ever differs between two identical calls, the agent
has stopped being deterministic and its receipts are worthless — the check is
there to catch that the day it happens.
deploy never announces. Putting a row in the Hub catalogue is an explicit,
outward-facing act; pass --announce when you actually mean it.
The agents
rules-decide — decisions you can argue with
Send a versioned rule set and a set of facts. Get the decision, the rule that fired, and a trace showing why each earlier rule did not.
{"policy": {"id": "refund@2026-09",
"rules": [{"id": "R1-window",
"when": [{"fact": "days_since_purchase", "op": "<=", "value": 14},
{"fact": "opened", "op": "==", "value": false}],
"then": {"decision": "approve", "reason": "unopened inside 14 days"}}],
"default": {"decision": "deny", "reason": "outside the refund window"}},
"facts": {"days_since_purchase": 31, "opened": true}}
Operators: == != < <= > >= in not_in matches exists.
Numeric comparison goes through Decimal(str(x)), so 0.1 + 0.2 problems
never reach a money threshold. A missing fact fails its condition and says so
rather than raising. The response carries policy_sha256 and facts_sha256,
so a receipt names exactly which policy version decided.
json-canonical — the same bytes everywhere
RFC 8785 (JCS) canonicalisation plus SHA-256/384. Keys sort by UTF-16 code unit, not code point — the two orders disagree above the BMP, which is where naive implementations quietly diverge.
It refuses floats and integers beyond 2^53-1. JCS serialises numbers with ECMAScript rules, so a float can canonicalise one way here and another way in a JavaScript verifier, and a large integer cannot survive that verifier at all. Emitting bytes that might not reproduce is worse than refusing. AWR receipts are integers-only for the same reason.
commit-referee — was the commitment actually binding
Checks a reveal against its commitment, and reports whether the scheme itself binds.
sha256(salt || value) with a variable-length salt is ambiguous: ("abc","def")
and ("ab","cdef") produce the same commitment, so a committer can still pick
which one to reveal after seeing the outcome. Layout lenprefix
(sha256(len(salt) || salt || value)) removes that. The referee verifies the
hash and flags the layout — see test_referee_flags_a_layout_that_opens_two_ways,
which demonstrates two different reveals verifying against one commitment.
Layouts: lenprefix (recommended), separator, concat, value.
Algorithms: sha256, sha384, sha512.
Limits worth knowing before writing a fourth
- A template handler is capped at 32 000 characters, and the request body at 256 KiB by default. Reference data — holiday calendars, price books, licence matrices — will not fit in the source and has to arrive in the payload, or the agent becomes a pinned image instead.
- Handlers are admitted by AST, which is admission control, not a sandbox:
an admitted handler still executes as a same-host subprocess. It is bounded —
a per-call deadline (10s), an address-space cap (512 MiB), a container mem/pids
limit and a response cap — but that is not cgroup isolation. For enforced
CPU/memory isolation run
HESTIA_RUNTIME=docker. - Priced handlers are charged now: see "Getting paid" below.
Getting paid
Payments are wired into the hearth; whether a tenant charges is the operator's
switch (HESTIA_PAYMENTS_ENABLED, off unless it is set). price_per_call_usd is
the price, and a priced agent charges it non-custodially.
- A priced call to
POST /t/{slug}/invoke(or the routed/ai-market/v2/invoke) returns 402 with x402-shaped terms naming the agent owner's own payout address, the amount in USDC base units, and the chain. - The 402 also carries a fresh
nonceand apayment_secret("binding": "eip3009"), with nonce = sha256(secret). The buyer pays with an EIP-3009transferWithAuthorizationsigned over that nonce, then retries withX-Payment: <tx hash>andX-Payment-Secret: <secret>. HESTIA reads the transfer and itsAuthorizationUsedlog on chain (hestia.payments), records it so one payment buys one call, and serves the result. The host holds no key and takes no cut — it reads the chain, it never touches the money. - Why the secret: once the payment is mined, its transaction hash and its nonce are public. Redeeming on those alone let anyone watching the chain present them first and take the call the buyer paid for, and the buyer was then refused as "already spent". The secret never goes on chain; the nonce is only its hash. Keep it to yourself until you call.
- Redeem promptly. The hearth stops redeeming a nonce when its invoice expires
(
expires_at, 900 s after the 402 unless the operator says otherwise), sopaynever builds a deadline past that and prints the time to redeem by. And a secret that sits in a file or a shell history is only worth taking until the call is served: after that the payment is spent, and the secret redeems nothing. - A 402 that binds nothing (
"binding": "none"), or binds a nonce but hands over nopayment_secret(a hearth from before commit-reveal), is refused byquote,payandcallwith "Do not pay it": whoever presented such a payment first would take the call, and the hearth itself no longer accepts one. - If the tenant is unreachable after payment is claimed, the claim is released so the same transaction can be retried.
payout_addressis set per agent at deploy (seehestia_agents/manifests.py);owner_pubkeystays the Ed25519 signing identity, which is a different thing.
Nothing here signs or holds a key. The flow is quote, pay in two steps, call:
hestia-agents quote --hearth $H --only json-canonical
# step 1: typed data to sign, plus the nonce, deadline, redeem-by time and
# payment secret it used
hestia-agents pay json-canonical --hearth $H --from 0xYOURADDRESS
# step 2: give back what step 1 printed with the signature; prints calldata
hestia-agents pay json-canonical --hearth $H --from 0xYOURADDRESS \
--nonce 0x… --valid-before 17… --amount 1000 --pay-to 0x… --signature 0x…
# send that calldata to the token contract from the signing address, then,
# before the redeem-by time
hestia-agents call json-canonical --hearth $H --tx 0x… --secret 0x…
quote shows the nonce and secret of its own 402, for a buyer who signs with
their own tools. pay asks for a 402 of its own, and every 402 mints a new
pair, so a payment pay builds is redeemed with the secret pay prints, never
with quote's; quote says so.
Step 2 takes the nonce, deadline, amount and payee back rather than asking
again: every 402 mints a new nonce and the signature covers all four, so
calldata rebuilt from a fresh 402 or a fresh clock is a transaction the token
contract reverts (step 2 used to do exactly that). The fresh 402 must still ask
that amount of that payee; after a redeploy between the steps it may not, and
what step 1 signed would no longer buy the call, so step 2 refuses and says to
run step 1 again. A hearth from before the secret sends no payment_secret;
quote and pay say so, and call --nonce redeems there.
Only a 200 is free and only a 402 that states its terms is a price: quote exits
1 on anything else (a 404 or 500 used to read as "free right now"), and on a
payment_secret that does not open its nonce or is guessable. pay refuses in
one line, in either step, a --from that is not an address, a negative
deadline, and a 402 whose payee, amount, chain id or token contract is missing
or malformed. Step 1 also refuses, before printing anything to sign, a
--valid-before later than the nonce's expiry, a nonce that has already
expired, a 402 whose nonce is malformed (or, on a hearth that binds, whose
expiry is), and a payment_secret that does not open its nonce or is guessable. Step 2 refuses, before printing any
calldata, a missing or malformed --nonce, --amount or --pay-to, a fresh
402 that asks another amount or payee, and a signature whose r/s are not 32
bytes of hex or whose v is not 27/28 (0/1).
A 402 answered to call --tx (or compute --tx) prints the hearth's refusal
and nothing else: the payment has been made, and the terms or a build-it line
printed after it read as an invitation to pay a second time for one call.
The secret need not go on a command line, where shell history and ps keep it:
--secret @file reads it from a file, and call --tx, compute --tx and
pay-compute --signature read HESTIA_PAYMENT_SECRET when no --secret is
given (a --secret that is given wins). Without --tx the environment is not
read, so a secret left there does not turn a free call into a refused one.
Commands printed for the next step name the secret the way it was given — a
file as the file, the environment not at all.
Compute and cross-hearth checks
A hearth with compute on (../hestia/docs/COMPUTE.md) sells these same pure
functions as hestia.compute.run@v1 (one measured replica) and
hestia.compute.verify@v1 (two replicas, signed only if they agree). This
package is also the buyer for that:
pip install 'aimarket-hestia-agents[compute]'
export HESTIA_COMPUTE_API_KEY=... # when the hearth sells to you by key
hestia-agents compute json-canonical --hearth https://a.example \
--input '{"document": {"b": 1}}' --replicated
hestia-agents cross-verify json-canonical --hearth https://a.example \
--hearth https://b.example --input @doc.json --verdict-out verdict.json
A hearth may also sell compute directly, to a buyer with no key: it names a
payout address used for compute and nothing else. That door mints no nonce —
on the production rail the hub is the till, and a second nonce would be a
second till one authorization cannot satisfy — so the buyer picks one, as
sha256 of a secret only they hold ("binding": "secret",
"nonce_rule": "sha256(secret)"). A plain transfer, or a secret that does not
open the nonce on chain, is refused at that door, for the same reason as above:
the transaction and its nonce are public once mined. A hub's keyed forward is
the hub's business: it names its own invoice's nonce and needs nothing from a
direct buyer.
# step 1: makes up the secret (or pass --secret 0x… or --secret @file), prints
# typed data to sign
hestia-agents pay-compute json-canonical --hearth https://a.example --from 0xYOURADDRESS
# step 2: the same --secret and --valid-before, with the signature; prints calldata
hestia-agents pay-compute json-canonical --hearth https://a.example --from 0xYOURADDRESS \
--secret 0x… --valid-before 17… --signature 0x…
# send it, then present the transaction and the secret, within the door's
# max_age_s of it being mined
hestia-agents compute json-canonical --hearth https://a.example --input @doc.json \
--tx 0x… --secret 0x…
Pass --replicated to both pay-compute and compute to buy the replicated SKU:
the price is per SKU, and what was signed is that price. pay-compute refuses, in
one line and before anything is signed, a hearth that does not bind by secret.
A secret you choose is held to the hearth's own rule first. The door refuses one
with fewer than 16 distinct bytes as guessable (32 random bytes hold about 30; a
pattern such as 0x00…00 is one anyone could precompute and open), and it would
say so only after you had paid under it. So pay-compute --secret,
compute --secret, compute.call(payment_secret=…) and call --secret refuse
such a secret in one line, before anything is asked, signed or sent; the ones
this tool makes up are drawn again in the (vanishingly rare) case they would be
refused. Step 1 of pay-compute never reads HESTIA_PAYMENT_SECRET: a secret
left in the environment would sign every new payment over one nonce, which the
token accepts only once.
Two replicas on one hearth are one operator checking itself (the receipt says
same_operator: true). cross-verify is the step past that: the same pinned
function_sha256 on two hearths with different keys, each receipt checked
against the key its hearth publishes, input and output digests recomputed here,
outputs compared. It refuses before paying anyone when both hearths share a key,
list different bytes under the slug, or either lists no function_sha256 at all
(two missing digests used to compare equal, and both hearths were paid unpinned). With the awr package installed,
--verdict-out writes an AWR VerificationVerdict over the first receipt.
Layout
agents/<slug>/handler.py the source the hearth executes
agents/<slug>/deploy.json generated deploy body — do not hand-edit
hestia_agents/manifests.py capability metadata and body generation
hestia_agents/cli.py build / deploy / verify / quote / pay / call /
compute / pay-compute / cross-verify
hestia_agents/compute.py compute buyer: receipt checks, cross-hearth comparison
hestia_agents/x402.py 402 -> typed data -> calldata; the payment secret rule
tests/ admission, behaviour, determinism
deploy.json embeds the handler source, so the two drift the moment anyone
edits one by hand. Regenerate with python -m hestia_agents.cli build; a test
asserts they match.
Licence
MIT. Part of the AICOM agent economy.
Metadata
Release files for aimarket-hestia-agents 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aimarket_hestia_agents-0.1.0.tar.gz | 76.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aimarket_hestia_agents-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 107.7 kB
Release files / aimarket_hestia_agents-0.1.0.tar.gz
| Download URL | aimarket_hestia_agents-0.1.0.tar.gz |
|---|---|
| Size | 76.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0432b6dc5a55543f5e7849e2599460539f5821de12d4e60e21a23f2ca5680af6
|
|
BLAKE2b-256 checksum How to use checksums |
1c78134d9e73f5819efd91e07ea76d20d9601ca8c78e12ca345d5142efb09198
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / aimarket_hestia_agents-0.1.0-py3-none-any.whl
| Download URL | aimarket_hestia_agents-0.1.0-py3-none-any.whl |
|---|---|
| Size | 31.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3557c6c349d1947b15d06bf0d0e33d8227559a284a88314a91d5bfd1399dacb9
|
|
BLAKE2b-256 checksum How to use checksums |
fb055aae3b86b1de2db2064d781d691695ba8c5155942b33c1acb92b57046aae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|