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) quickstart — full L402 round-trip, no real Lightning node.
//!
//! A `Paywall` issues a 402 with a DID-signed BOLT11 invoice. A client wallet
//! (another `MemoryNode` sharing the same in-memory ledger) pays the invoice,
//! retries with `Authorization: L402 <token>:<preimage>`, and the paywall hands
//! back a signed `X-Payment-Receipt` plus the gated payload.
//!
//! Run with: `cargo run --example quickstart`

use std::collections::HashMap;
use std::sync::Arc;

use agent_pay::{
    did_key_from_public_key, fetch_with_l402, generate_key_pair, FetchFn, FetchOptions,
    FetchResponse, InnerHandler, LightningNode, MemoryLedger, MemoryNode, Paywall, PaywallOptions,
    PaywallResponse,
};

const SECRET: &[u8] = b"thirty-two-byte-test-secret-pad!";

#[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 opts = PaywallOptions::new(
        did.clone(),
        kp.private_key,
        1000,
        "/report",
        server,
        SECRET.to_vec(),
    );
    let paywall = Arc::new(Paywall::new(opts));

    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()
            })
        })
    });

    let pay = paywall.clone();
    let fetch: FetchFn = Arc::new(move |url: String, headers: HashMap<String, String>| {
        let pay = pay.clone();
        let handler = handler.clone();
        Box::pin(async move {
            let path = url
                .split_once("://")
                .and_then(|(_, rest)| rest.split_once('/'))
                .map(|(_, p)| format!("/{p}"))
                .unwrap_or_else(|| url.clone());
            let resp = pay.process_request(&path, headers, Some(handler)).await?;
            Ok(FetchResponse {
                status: resp.status,
                headers: resp.headers,
                body: resp.body,
                json: resp.json,
            })
        })
    });

    let mut fopts = FetchOptions::new(client, 5000, fetch);
    fopts.expected_did = Some(did.clone());

    println!("server DID: {did}");
    let res = fetch_with_l402("http://x/report", fopts).await.unwrap();
    let receipt = res.header("x-payment-receipt").unwrap_or("");
    println!("status:  {}", res.status);
    println!(
        "payload: {}",
        res.json
            .as_ref()
            .map(|v| v.to_string())
            .unwrap_or_default()
    );
    let head = &receipt[..receipt.len().min(64)];
    let suffix = if receipt.len() > 64 { "..." } else { "" };
    println!("receipt: {head}{suffix}");
}