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 Spec License

Idiomatic Rust port of @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

[dependencies]
agent-pay = "0.1"

Quickstart

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:

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 reference
Python agent-pay-py port
Rust agent-pay (this) port

Conformance

This port passes the same C1C4 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.
  • e2ePaywall + MemoryNode round-trip end to end (tests/e2e.rs).
cargo test

Vectors live in 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 for why.

Architecture

See docs/architecture.md.

Development

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.