diff --git a/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs b/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs index 620bdc668..04329c8a1 100644 --- a/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs +++ b/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs @@ -8,7 +8,7 @@ use ed25519_dalek::SigningKey; use jcp_schema::{Did, Funds, Verdict}; -use jcp_x402::{ingest_requirements, Gated, X402Profile, X402Requirements}; +use jcp_x402::{ingest_requirements, Gated, X402Profile, X402Requirements, X402Settlement}; use serde_json::json; fn main() { @@ -64,7 +64,42 @@ fn main() { let eurc = X402Requirements { asset: "EURC".into(), max_amount: 100, ..req.clone() }; println!("charge in EURC (grant is USDC) → {}", verdict(&profile.authorize(&eurc, 1))); - // 6. Every decision sealed a JCR-1 receipt that verifies under the steward + // 6. THE COUPLING. The host settles the authorized charge on chain and gets + // back a transaction; the meter reports what the paid compute actually + // consumed. 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. + let settled = X402Settlement { + transaction: Some("0xdeadbeefcafe".into()), + network: "base".into(), + payer: Some("0xAgentWallet".into()), + }; + let measured_uj = 5_812; // what the meter read, not the 6000 µJ estimate + let g = profile.seal_settlement(&req, &settled, measured_uj); + println!("\nsettled {} → sealed {}", settled.transaction.as_deref().unwrap_or("-"), verdict(&g)); + + // 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, measured_uj).to_hex(); + println!( + " coupling verified: {} (tx + {} µJ bound into payload_hash {}…)", + core.payload_hash.as_deref() == Some(expected.as_str()), + measured_uj, + &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, measured_uj).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();