Metadata-Version: 2.4
Name: aotrust-protocol
Version: 2.3.7
Summary: Asyncio-native SDK for A&O Trust Layer — Agent Notary Service
Author: A&O Trust Layer
License: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Framework :: AsyncIO
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9
Requires-Dist: pydantic>=2.0
Requires-Dist: pynacl>=1.5
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: aioresponses>=0.7; extra == "dev"
Dynamic: license-file

# aotrust-protocol SDK

<!-- mcp-name: link.aotrust/notary -->

Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.

## Install

```bash
pip install aotrust-protocol
```

## Quickstart

### Free tier — no API key, no wallet (fastest way to try)

```python
import asyncio, hashlib
from agent_notary import NotaryClient

async def main():
    # IMPORTANT: no /v1 suffix — the SDK appends API paths itself.
    client = NotaryClient(base_url="https://api.aotrust.link")

    work_hash = hashlib.sha256(b"my artifact text").hexdigest()
    result = await client.shield_free(work_hash)
    job_id = result["job_id"]          # keep this UUID — it is the handle
    print("job_id:", job_id)

    # Check status BY job_id (not by the artifact text!)
    status = await client.get_status(job_id)
    print("status:", status.status)    # PENDING → anchored (anchor batches)

    # Fetch + verify the PDR
    pdr = await client.get_pdr(job_id)
    print("verify in browser:", pdr.verify_url)
    assert (await client.verify_pdr(pdr.pdr_b64))["valid"]

asyncio.run(main())
```

Free tier: 5 PDR / 24h per IP. Examples: [`examples/04_free_tier.py`](examples/04_free_tier.py).

### Witness mode (pre-paid tx_hash)

```python
import asyncio
from agent_notary import NotaryClient, NotarizeRequest

async def main():
    client = NotaryClient(
        api_key="your-api-key",
        base_url="https://api.aotrust.link"  # no /v1 suffix!
    )

    # Submit notarization
    req = NotarizeRequest(
        tx_hash="EzrfDW5b...",
        work_hash="599d6999...",
        agent_sig="base64_sig_A...",
        agent_pubkey="aff91a18...",
    )
    result = await client.notarize(req)
    print(f"Job: {result.job_id}")

    # Poll for PDR — status/pdr calls always take the job_id returned above
    status = await client.wait_for_pdr(result.job_id, timeout=60)
    if status.is_anchored:
        pdr = await client.get_pdr(result.job_id)
        assert pdr.verify(client.notary_pubkey)
        print("PDR Valid: YES")

asyncio.run(main())
```

> **Why no `/v1`?** `base_url` must be `https://api.aotrust.link`. The SDK
> already builds `/v1/...` request paths; adding `/v1` yourself produces
> `/v1/v1/...` → HTTP 404 on every call. This bit a real user (2026-09-29:
> quickstart copy-paste + status lookups by artifact text instead of job_id).

## Configuration

| Env Var | Constructor Arg | Description |
|---------|-----------------|-------------|
| `NOTARY_API_KEY` | `api_key` | API key for auth |
| `NOTARY_API_URL` | `base_url` | API base URL |
| `NOTARY_PUBKEY` | `notary_pubkey` | Notary Ed25519 public key (hex) |

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| POST | `/v1/notarize` | Submit notarization |
| GET | `/v1/status/{job_id}` | Poll job status |
| GET | `/v1/pdr/{job_id}` | Get PDR |
| POST | `/v1/notarize/quote` | Get price quote |
| GET | `/.well-known/agent.json` | MCP Server Card |
| GET | `/openapi.json` | OpenAPI 3.0 spec |

## PDR Verification

```python
pdr = await client.get_pdr(job_id)
is_valid = pdr.verify(notary_pubkey_hex="9c7d64bb...")
```

Uses NEP-413 raw-buffer verification. No SHA256 pre-hash.

## Error Handling

```python
from agent_notary.exceptions import (
    NotaryAuthError,        # 401 (authenticated CI surfaces; anonymous x402 gives 402 instead)
    NotaryPaymentError,     # 402 x402-challenge / 403
    NotaryNotFoundError,    # 404
    NotaryValidationError,  # 400/422 — invalid params, INVALID_AGENT_SIGNATURE
    NotaryConflictError,    # 409 — tx_hash/job_id already used (idempotency)
    NotaryServerError,      # 5xx, carries status_code
    NotaryError,            # base — safety net
)

try:
    result = await client.notarize(req)
except NotaryAuthError:
    print("Invalid API key")
except NotaryPaymentError as e:
    print(f"Payment/sig failed: {e}")
except NotaryValidationError as e:
    print(f"Bad request or invalid signature: {e}")
except NotaryConflictError:
    print("This tx_hash/job_id was already used — generate a new one")
except NotaryError as e:
    print(f"Other SDK error: {e}")
```

## Development

```bash
pip install -e ".[dev]"
pytest tests/ -v
```
