Metadata-Version: 2.4
Name: aotrust-protocol
Version: 2.4.0
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.

> **Why protocol-level proofs?** An ERC-8004 empirical study (2026) measured that **>98% of agent feedback records carry no proof of an underlying interaction** (no payment proof, no task linkage); 59–91% of reviewers on three chains participated in coordinated Sybil campaigns, and manipulating a reputation score past a trust threshold costs as little as **$0.0027**. The agentic economy is moving from *statements* to *verifiable claims*: a PDR (239 bytes, Ed25519-signed, hash-chained, NEAR-anchored) turns any output or payment into independent evidence — verifiable by anyone, without trusting the provider's database.
> *(Refs: arXiv:2610.12948 [cs.CR], 2026; ERC-8004 Sybil measurements, Xiong et al. 2026.)*

## 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
```

## Payment binding (NEAR rail)

NEAR payments must be provably linked to the account that sent them. Pass
`payer_account` (the NEAR account that paid) together with `bind_sig` — a
base64 Ed25519 **NEP-413 raw-buffer** signature (no SHA-256 pre-hash) over:

```
AOTrust AITP-BIND v1
work_hash:<64 hex>
tx_hash:<tx hash exactly as submitted>
```

The server resolves the account's public key via RPC and checks
`transaction.signer_id == payer_account`; a mismatch is rejected with
`403 PAYMENT_BIND_SIGNER_MISMATCH`, a bad signature with
`403 PAYMENT_BIND_INVALID`.

If `NEAR_PRIVATE_KEY` is set in the environment (or `near_private_key=` is
passed), the SDK signs the canonical message itself and fills `bind_sig`:

```python
req = NotarizeRequest(tx_hash=tx, work_hash=wh)
await client.notarize(req, payer_account="alice.near")
```

Server mode `AITP_BIND_MODE=warn` (default) accepts unbound requests with a
`PAYMENT_UNBOUND` log line; `enforce` requires the fields.
