Metadata-Version: 2.4
Name: virohana-agent
Version: 0.1.0
Summary: Thin client for buying work from Virohana over x402: discover the catalog, parse the 402 quote, verify the receipt offline.
Author-email: Virohanalife OU <support@virohanalife.com>
License-Expression: MIT
Project-URL: Homepage, https://www.virohanalife.com/agents
Project-URL: Documentation, https://www.virohanalife.com/agents
Project-URL: Machine-readable index, https://www.virohanalife.com/llms.txt
Project-URL: Public receipts, https://labor.virohanalife.com/v1/receipts
Keywords: x402,usdc,agent,a2a,mcp,ed25519,receipts,402
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41
Dynamic: license-file

# virohana-agent

Thin Python client for buying work from [Virohana](https://www.virohanalife.com/agents)
machine to machine: **discover** the catalog, **parse** the x402 v2 quote,
**verify** the receipt offline against a pinned Ed25519 key.

*Sovereign, humanity-first AI. Own the AI. Own your life.*

```bash
pip install virohana-agent
```

## What it does — and what it deliberately does not

| | |
| --- | --- |
| Discover the labor + memory catalogs | ✅ |
| Parse an HTTP 402 into a typed x402 v2 quote | ✅ |
| Verify a receipt's Ed25519 signature, **offline** | ✅ |
| Sign and send the payment | ❌ — use [`x402`](https://pypi.org/project/x402/) |

Paying means holding a wallet key. A discovery client is the wrong place for
one, so this package never asks for it. Hand the parsed quote to the thing
that already owns your key.

## Discover

```python
from virohana_agent import Catalog

cat = Catalog.fetch()                       # two plain GETs, no auth, no account
print(len(cat), "skills")

s = cat.get("summarize", service="labor")
print(s.sku, s.price_usdc, s.url)           # summarize 0.03 https://labor…/v1/summarize
```

`summarize` and `notarize` exist on **both** services at different prices, so
`get()` raises rather than picking one for you. `service=` disambiguates.

## Quote

```python
quote = cat.quote("summarize", {"text": "…"}, service="labor")
req = quote.cheapest()

req.amount_usdc        # 0.03
req.amount_atomic      # 30000  (USDC is 6 decimals)
req.network            # 'eip155:8453'  — Base
req.pay_to             # '0x…'
req.is_usdc_on_base    # True
req.input_schema       # {'type': 'object', 'required': ['text'], …}
```

Quoting is free and commits to nothing: an unpaid request *is* how x402
quoting works. The client refuses anything that is not x402 **v2** rather
than guessing at another version's field names.

## Verify — offline, always

```python
from virohana_agent import verify_receipt

result = verify_receipt(receipt, response_body_bytes)
result.settlement_url()     # https://basescan.org/tx/0x…  — for you to open
```

Three checks, in the order they fail cheaply:

1. **Commitment** — `sha256(body) == receipt.response_sha256`. Proves the
   receipt is about *your* bytes.
2. **Signature** — Ed25519 over the raw response bytes.
3. **Pinning** — the signing key is one this package shipped with.

(3) is what makes (2) worth doing. Verifying a signature against a key the
receipt itself handed you proves only that the receipt is internally
consistent. The pinned keys are in `virohana_agent.receipts`; rotating one
means a new release of this package, on purpose.

Nothing in the verify path touches the network. A verifier that asks the
seller *"is this receipt valid?"* proves nothing — a dishonest seller answers
yes. Settlement is the one check left to you: `settlement_url()` gives you
the explorer link, because confirming it would mean an RPC call this module
promises not to make.

`body` must be the **raw bytes off the wire**. Re-serialising parsed JSON
changes key order and whitespace, and the signature will not verify — that is
the signature working, not a bug. Passing a `str` raises for exactly this
reason.

## Offline use

Everything except `Catalog.fetch()` and `Catalog.quote()` works with no
network at all:

```python
cat = Catalog.from_payloads(labor=my_cached_x402, memory=my_cached_catalog)
```

## Tests

```bash
PYTHONPATH=src python3 -m unittest discover -s tests -v
```

24 tests, no network. Fixtures are real payloads captured from the live
services — a synthetic fixture would let this client drift away from what the
servers actually send, which is the failure the suite exists to catch.

## Links

- [Buy work from Virohana, machine to machine](https://www.virohanalife.com/agents)
- [llms.txt](https://www.virohanalife.com/llms.txt) · [trust.json](https://www.virohanalife.com/trust.json)
- [Live discovery](https://labor.virohanalife.com/.well-known/x402) · [public receipts](https://labor.virohanalife.com/v1/receipts)

MIT. Virohanalife OÜ, Tallinn, Estonia.
