agent-pay 0.1.0

L402 + DID-signed invoices: agent-to-agent Lightning payments (Rust port of @p-vbordei/agent-pay)
Documentation
# agent-pay (Rust)

[![CI](https://github.com/p-vbordei/agent-pay-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/p-vbordei/agent-pay-rs/actions/workflows/ci.yml)
[![Spec](https://img.shields.io/badge/spec-v0.1-blue)](./SPEC.md)
[![License](https://img.shields.io/badge/license-Apache%202.0-green)](./LICENSE)

> **Idiomatic Rust port of [`@p-vbordei/agent-pay`]https://github.com/p-vbordei/agent-pay.** L402 + DID-signed invoices for agent-to-agent Lightning payments. Wire-compatible with the TS reference; ships an in-memory mock LND so tests and demos run without a real node.

## What's in the box

- `Paywall::process_request(path, headers, inner)` — L402 challenge/response middleware, framework-agnostic.
- `Token` — L402 macaroon-style bearer token (HMAC SHA-256 `{payload}.{hmac}`).
- `BOLT11` — parse + encode Lightning invoices via the `lightning-invoice` crate (with the modern `payment_secret` tag).
- `JWS` — DID-bound envelope (Ed25519 + JCS-canonical headers/payloads).
- `MemoryNode` — mock LND for tests and offline demos (shared `MemoryLedger` between server and client wallets).
- `LndRest` — real LND adapter (optional, skipped unless `AGENT_PAY_INTEGRATION=1`).
- `ReplayCache` — preimage-based replay protection keyed by `payment_hash`.

## Install

```toml
[dependencies]
agent-pay = "0.1"
```

## Quickstart

```rust
use std::sync::Arc;
use agent_pay::{
    did_key_from_public_key, fetch_with_l402, generate_key_pair, FetchOptions, InnerHandler,
    LightningNode, MemoryLedger, MemoryNode, Paywall, PaywallOptions, PaywallResponse,
};

#[tokio::main]
async fn main() {
    let kp = generate_key_pair();
    let did = did_key_from_public_key(&kp.public_key).unwrap();
    let ledger = MemoryLedger::new();
    let server: Arc<dyn LightningNode> = Arc::new(MemoryNode::new(ledger.clone(), "server"));
    let client: Arc<dyn LightningNode> = Arc::new(MemoryNode::new(ledger.clone(), "client"));

    let paywall = Arc::new(Paywall::new(PaywallOptions::new(
        did.clone(), kp.private_key, 1000, "/report", server,
        b"thirty-two-byte-test-secret-pad!".to_vec(),
    )));
    let handler: InnerHandler = Arc::new(|_p, _h| Box::pin(async move {
        Ok(PaywallResponse { status: 200,
            json: Some(serde_json::json!({ "insight": "agents charging agents works." })),
            ..Default::default() })
    }));
    // see examples/quickstart.rs for the FetchFn wiring
}
```

Run the full demo:

```bash
cargo run --example quickstart
```

```
server DID: did:key:z6Mk…
status:  200
payload: {"insight":"agents charging agents works."}
receipt: eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa…
```

## How it relates

| Language | Package | Source of truth |
| --- | --- | --- |
| TypeScript | [`@p-vbordei/agent-pay`]https://github.com/p-vbordei/agent-pay | reference |
| Python | [`agent-pay-py`]https://github.com/p-vbordei/agent-pay-py | port |
| Rust | `agent-pay` (this) | port |

## Conformance

This port passes the same `C1`–`C4` clauses as the TS reference and adds a full e2e round-trip:

- **C1-missing** — client rejects 402 missing `X-Did-Invoice`.
- **C1-bad-sig** — client rejects 402 with a tampered `X-Did-Invoice` JWS.
- **C2** — happy-path: valid invoice paid → 200 with verified `X-Payment-Receipt`.
- **C3** — server rejects a replayed preimage with 401.
- **C4** — client rejects when `invoice_hash` mismatches BOLT11.
- **e2e**`Paywall` + `MemoryNode` round-trip end to end (`tests/e2e.rs`).

```bash
cargo test
```

Vectors live in [`vectors/`](./vectors) and are byte-identical to the TS suite's `conformance/vectors/` for the JWS + token layers. BOLT11 wire bytes differ across libraries even with identical contents — see [`docs/architecture.md`](./docs/architecture.md#bolt11-wire-format-caveat) for why.

## Architecture

See [docs/architecture.md](./docs/architecture.md).

## Development

```bash
git clone https://github.com/p-vbordei/agent-pay-rs
cd agent-pay-rs
cargo test
cargo clippy --all-targets -- -D warnings
```

## License

Apache-2.0 — see [LICENSE](./LICENSE).