//! Ingest an x402 PaymentRequirements, mint a dual-ceiling JCP grant (a USDC //! session cap + an operator joule cap), and gate live charges — every decision //! sealed, no USDC→joule exchange rate ever invented. //! //! ```text //! cargo run -p jcp-x402 --example pay //! ``` use ed25519_dalek::SigningKey; use jcp_schema::{Did, Funds, Verdict}; use jcp_x402::{ingest_requirements, Gated, X402Profile, X402Requirements, X402Settlement}; use serde_json::json; fn main() { // 1. A server's `402` challenge: pay 1.00 USDC (6 decimals) on Base to a // merchant for a premium inference endpoint. let req = ingest_requirements(&json!({ "scheme": "exact", "network": "base", "maxAmountRequired": "1000000", "resource": "/api/premium-inference", "payTo": "0xMerchantAddress", "asset": "USDC" })) .expect("valid x402 requirements"); println!( "x402 charge: {} minor {} on {} → {} for {}", req.max_amount, req.asset, req.network, req.pay_to, req.resource ); // 2. Mint a dual-ceiling grant. The PAYMENT bounds the money (a 2.50 USDC // session cap); the OPERATOR bounds the energy (a joule ceiling the // payment has no authority over). Two ceilings, two sources of authority // — which is exactly why they are never converted into one another. let principal = SigningKey::from_bytes(&[1u8; 32]); // authorizes the spend let agent = Did::from_verifying_key(&SigningKey::from_bytes(&[2u8; 32]).verifying_key()); let mut profile = X402Profile::new( "base", "0xMerchantAddress", 20_000_000, // joule ceiling (µJ) — operator-set Funds::new("USDC", 2_500_000), // money ceiling — 2.50 USDC session cap 5_000_000, // per-call joule ceiling — holds stay concurrent &principal, agent, SigningKey::from_bytes(&[7u8; 32]), "did:example:x402-steward", ) .expect("mint grant"); let (net, merch) = profile.merchant(); println!("minted a dual-ceiling grant for {net}:{merch} — 2.50 USDC + 20 mJ\n"); // 3. Two 1.00 USDC charges fit the 2.50 cap → allowed; both dimensions debit. for n in 1..=2 { let g = profile.authorize(&req, 6_000); println!("charge #{n} (1.00 USDC, 6000 µJ) → {}", verdict(&g)); } // 4. A third would total 3.00 > 2.50 → denied on MONEY while joules still had // room. The receipt records which ceiling bound — the statement a // single-dimension authorization cannot make. let g = profile.authorize(&req, 6_000); println!("charge #3 (would total 3.00 USDC) → {}", verdict(&g)); // 5. A charge in a different asset is refused, not converted. let eurc = X402Requirements { asset: "EURC".into(), max_amount: 100, ..req.clone() }; println!("charge in EURC (grant is USDC) → {}", verdict(&profile.authorize(&eurc, 1))); // 6. THE COUPLING. The host settles the authorized charge on chain and gets // back a transaction; its meter reports what the paid compute consumed, // at the grade it read it. Sealing them together produces the artifact // neither side has: the payment rail knows the transaction but not the // energy, an energy meter knows the joules but not the transaction. // Here no transaction settles (its hash is illustrative). The paid // compute is a small workload run here, metered by this host's meter as // it ran, and committed at the grade the meter gave it: a hardware // reading only where a live counter read the work, else the estimate, // which the record names as one. let settled = X402Settlement { transaction: Some("0xdeadbeefcafe".into()), network: "base".into(), payer: Some("0xAgentWallet".into()), settled_amount: None, }; let meter = jcp_energy::Meter::detect(); let (_, energy) = meter.measure(paid_compute); let g = profile.seal_settlement(&req, &settled, energy); println!("\nsettled {} (illustrative tx) → sealed {}", settled.transaction.as_deref().unwrap_or("-"), verdict(&g)); println!( " paid compute: {} µJ [{}], metered here by the {:?} rung", energy.uj(), energy.provenance.jcr1_label(), meter.rung() ); // A third party verifies the binding without trusting the gateway: it // recomputes the coupling id from the settlement record it holds and // checks it against the receipt's payload_hash (jcp/2, SPEC §8). let core = jcp_receipt::extract(&g.receipt, profile.steward(), &profile.steward_pubkey()) .expect("settlement receipt verifies"); let expected = settled.coupling_id(&req.resource, &energy).to_hex(); println!( " coupling verified: {} (tx + {} µJ [{}], bound into payload_hash {}…)", core.payload_hash.as_deref() == Some(expected.as_str()), energy.uj(), energy.provenance.jcr1_label(), &expected[..16] ); // Re-pointing the receipt at a different transaction — or a different // energy figure — yields a different id, so the binding cannot be moved. let other = X402Settlement { transaction: Some("0xfeed".into()), ..settled.clone() }; println!( " re-point to another tx → id matches: {}", core.payload_hash.as_deref() == Some(other.coupling_id(&req.resource, &energy).to_hex().as_str()) ); // 7. Every decision sealed a JCR-1 receipt that verifies under the steward // key — a third party trusts the decision, not the gateway. let steward = profile.steward().to_string(); let pk = profile.steward_pubkey(); let verified = profile .receipts() .iter() .filter(|r| jcp_receipt::extract(r, &steward, &pk).is_ok()) .count(); println!( "\nsealed receipts: {} (all {} verify under the steward key)", profile.receipts().len(), verified ); println!( "spent: {} µJ of 20 mJ · {} USDC-minor of the 2.50 cap", profile.spent_uj(), profile.spent_minor() ); } /// The paid compute: a fixed amount of real CPU work on this thread, so the /// meter has something to read. fn paid_compute() -> u64 { let mut x = 0x9e37_79b9_7f4a_7c15u64; for i in 0..5_000_000u64 { x = std::hint::black_box(x.rotate_left(5) ^ i).wrapping_mul(0x0000_0100_0000_01b3); } x } /// Read the verdict the way a verifier would — off the sealed decision. fn verdict(g: &Gated) -> String { match g.decision.verdict { Verdict::Allow => "ALLOW".into(), Verdict::Deny(r) => format!("DENY ({})", r.label()), } }