| hermes_nano_x402 | ||
| hermes_nano_x402.egg-info | ||
| tests | ||
| LICENSE | ||
| PKG-INFO | ||
| pyproject.toml | ||
| README.md | ||
| setup.cfg | ||
hermes-nano-x402
A Hermes Agent plugin that buys x402 pay-per-call resources with Nano (XNO) on
nano:mainnet (scheme: exact) from inside the agent loop — with a payment guard that runs
before anything is signed.
Hermes is the framework binding; the signing primitives are pure Python (no compiled crypto
dependency), so pip install works on any machine the agent runs on.
- Framework: Hermes Agent (
hermes_agent.pluginsentry point + a drop-in plugin directory) - Registry: PyPI —
pip install hermes-nano-x402(install scripts off, no build step) - License: MIT
- Verified seller used in the example: pursekeeper
/v1/fetchat Ӿ0.001, catalogue/.well-known/x402
Install
pip install hermes-nano-x402 # from the public registry, scripts off by default
hermes plugins enable hermes-nano-x402 # plugins are opt-in in Hermes
Or straight from the repository:
git clone https://git.kaki87.net/x402assef/hermes-nano-x402
pip install ./hermes-nano-x402
hermes plugins enable hermes-nano-x402
The plugin registers one tool, nano_x402_fetch, into the nano_x402 toolset.
Configure (environment only — the package never holds a key)
export NANO_SEED=<64 hex> # the signer; never logged, never printed
export NANO_X402_ALLOW_PAYTO=nano_1xug1q5t7nxoj3ywwzokiea9jz8fq8qfgzp8pbyfr3co3e5xgj755uofu8ue
export NANO_X402_ALLOW_ORIGINS=https://pursekeeper.dev
export NANO_X402_MAX_PAY=0.01 # per-call cap, XNO
export NANO_X402_MAX_TOTAL=0.05 # cumulative cap for the process/run, XNO
# optional: NANO_INDEX=0, NANO_RPC=https://<node>, NANO_ACCOUNT_API=https://pursekeeper.dev
export NANO_PUBLISH=1 # broadcast the signed block; unset = dry run (sign only)
export NANO_X402_PUBLISH_RPC=https://rpc.nano.to # node used for `process`
NANO_X402_ALLOW_PAYTO is the operator allowlist. Empty allowlist = every quote is refused.
The one-call example (agent loop, not a script beside it)
Start Hermes and ask it, in the normal loop:
Use nano_x402_fetch on
https://pursekeeper.dev/v1/echo?msg=helloand show me what came back.
Hermes calls the tool the way it calls any other:
{"tool": "nano_x402_fetch",
"arguments": {"url": "https://pursekeeper.dev/v1/echo?msg=hello"}}
and the tool returns
HTTP 200 | paid 0.001 XNO | block <BLOCK HASH>
{"echo":"hello", ...}
A paid resource worth trying next: https://pursekeeper.dev/v1/fetch?url=https://example.com
(the catalogue is https://pursekeeper.dev/.well-known/x402).
Under the hood, in the agent process: unsigned GET → 402 with PAYMENT-REQUIRED →
:class:Quotevalidation →PaymentGuard.authorize→ one signed send block →PAYMENT-SIGNATURE: base64({x402Version, resource, accepted, payload:{block}}) → the block is published to a Nano node (process, do_work: true) behind a **publish gate**, then the signed request is re-presented and the seller verifies and serves. NANO_PUBLISHunset means the block is signed and *not* broadcast — the dry-run mode used in the tests and inexamples/live_probe.py`.
The guard (rule 3, each line has a test)
| Refusal | Trigger |
|---|---|
wrong_network |
the offer is not exact on nano:mainnet |
payto_not_allowed |
payTo is not in NANO_X402_ALLOW_PAYTO |
origin_not_allowed |
the quote's origin is not in NANO_X402_ALLOW_ORIGINS |
amount_above_cap |
quote > NANO_X402_MAX_PAY |
cumulative_cap |
signed total + quote > NANO_X402_MAX_TOTAL |
duplicate_request |
this request already has a block — it is re-presented, never signed twice |
quote_expired |
older than min(maxTimeoutSeconds, NANO_X402_MAX_QUOTE_AGE) — re-fetched, never reused |
redirect_refused |
a redirect to another origin (nothing sent there, nothing signed) |
work_required |
extra.workOptional is false and no work provider is configured |
insufficient_balance |
the account cannot cover the quoted amount |
malformed_quote |
bad base64, not v2, non-integer amount, zero amount, bad maxTimeoutSeconds, bad payTo |
publish_failed joins the guard when a settle step is configured: a destination outside the
publish allowlist, an amount over the publish cap, a link that is not the quoted account, an
unsigned block, or a node rejection means the signed request is never sent.
Also enforced: one send block per call; no paid retry after a non-2xx; signed requests never
follow redirects; extra.workOptional sellers are tried with work: "0" first (they may compute
it), and if the node answers "Block work is less than threshold" the work is computed offline in
hermes_nano_x402/work.py and the same signed block is published again — work is not covered by
the signature, so no second block is ever signed. NANO_LOCAL_WORK=0 disables that fallback, and
NANO_WORK_TIMEOUT (seconds, default 1800) bounds it.
Verify it yourself
pip install "hermes-nano-x402[dev]"
python -m pytest -q # 67 tests, all offline against a mocked 402 and a mocked node
python examples/live_probe.py # reads a real 402, runs the guard, signs — broadcasts nothing
tests/test_nano_crypto.py pins the block hash and signature to the reference implementation
(nanocurrency, npm) for a fixed seed, so the signing path cannot silently drift.
What the package contains
hermes_nano_x402/
__init__.py plugin entry point: register(ctx) -> tool nano_x402_fetch
client.py 402 -> guard -> one signed block -> re-present
guard.py caps, allowlists, dedupe, redirect policy
quote.py x402 v2 PAYMENT-REQUIRED parsing/validation
signer.py EnvSigner: seed from the environment, never printed
publish.py publish the signed block (process RPC) behind its own gate, dry run by default
nano_crypto.py ed25519-blake2b, base32 addresses, state-block hashes (pure Python)
plugin.yaml Hermes manifest
examples/live_probe.py
tools/gen_claim_account.py offline keypair -> nano_ address for a bounty claim
tests/ 67 offline tests
No seed, private key or generated key material is stored, logged or printed anywhere in the
package (tests/test_no_secrets.py scans for it).
License
MIT — see LICENSE.