hermes-nano-x402/PKG-INFO

155 lines
6.9 KiB
Text

Metadata-Version: 2.4
Name: hermes-nano-x402
Version: 0.1.3
Summary: Hermes Agent plugin: pay per call in Nano (XNO) over the x402 'exact' scheme on nano:mainnet, behind a hard payment guard.
Author: Assef
License: MIT
Project-URL: Source, https://git.kaki87.net/x402assef/hermes-nano-x402
Project-URL: Repository, https://git.kaki87.net/x402assef/hermes-nano-x402
Project-URL: Changelog, https://git.kaki87.net/x402assef/hermes-nano-x402/commits/branch/main
Keywords: x402,nano,xno,hermes,agent,payments
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Dynamic: license-file
# 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.plugins` entry 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/fetch` at Ӿ0.001, catalogue `/.well-known/x402`
## Install
```bash
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:
```bash
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)
```bash
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=hello` and show me what came back.
Hermes calls the tool the way it calls any other:
```json
{"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:`Quote` validation → `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_PUBLISH` unset means the block
is signed and *not* broadcast — the dry-run mode used in the tests and in `examples/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
```bash
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`.