# agent-pay (Rust)
[](https://github.com/p-vbordei/agent-pay-rs/actions/workflows/ci.yml)
[](./SPEC.md)
[](./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
| 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).