155 lines
6.9 KiB
Text
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`.
|