open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/openpay_per_joule_billing.rs
Transaction Science 899ca0b97c jouleclaw: cross-standard composition demo (JouleClaw → JCR-1 → OpenPay)
The open-standards quintet's commercial pitch made real. Before
this commit the README claimed JouleClaw + JCR-1 + OpenPay + WAI
+ ARL + Sandbox + Smart Byte + AAC compose into one stack, but
nothing in the workspace ran a multi-standard pipeline end-to-end.

The new example, openpay_per_joule_billing.rs, runs three
standards in one binary:

  1. JouleClaw cascade walk through JouleClawStack::with_defaults
     — counts every joule actually spent (one L3 + one L0-cache
     hit, ~2 J total).
  2. JCR-1 receipt minted over the session via the wave-7
     canonical det-CBOR + COSE_Sign1 path (335 B core / 456 B
     envelope, signed by the published test steward key).
  3. OpenPay settlement Transaction emitted in op-ledger v1's
     serde shape (status, ledger_id, entries[] with proper
     Money/Currency/Direction encoding, metadata pinning the
     JCR-1 receipt id) — consumable by any conforming OpenPay
     ledger via serde_json::from_value::<Transaction>.

Plus a binding document linking the receipt id to the Transaction
id by an explicit, verifiable invariant: receipt.joules_uj /
100_000 == settlement.amount_minor_units (1 ¢ per 100 mJ = $0.0001
per J pricing). An auditor verifies the JCR-1 signature, runs the
binding invariant, and the OpenPay Transaction is provably grounded
in measured energy.

Why no op-ledger import: OpenPay lives in a sibling Cargo
workspace; cross-workspace path deps don't carry workspace.package
inheritance, so direct cross-linking fails. The DEEPER reason for
NOT importing is that this proves the right kind of composition —
standards compose at the WIRE LAYER (serde_json), not at the
crate-graph layer. Standards in different language ecosystems
compose this way by construction.

Three artifacts written to /tmp/:
  openpay-per-joule-receipt.json    — JCR-1 signed receipt
  openpay-per-joule-entry.json      — OpenPay Transaction
  openpay-per-joule-binding.json    — receipt ↔ entry binding

Self-checks the invariant on exit; aborts if it doesn't hold.

WAI / ARL / Sandbox compositions are roadmap, not part of this
commit — same pattern (wire-layer serde_json), different
standards. Adding them is mechanical.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 03:58:20 -04:00

238 lines
9.6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `openpay_per_joule_billing` — cross-standard composition demo.
//!
//! The pay-per-joule pitch made real. A single binary that:
//!
//! 1. Runs a JouleClaw cascade walk through `JouleClawStack::with_defaults`
//! and counts every joule actually spent.
//! 2. Mints a signed **JCR-1** receipt over the session via the wave-7
//! canonical det-CBOR + COSE_Sign1 path.
//! 3. Extracts `joules_uj` from the receipt, applies a deterministic
//! price function, and emits an **OpenPay**-shape settlement entry
//! (the same wire form `op-ledger::Entry` accepts).
//! 4. Writes a tiny "binding" document that links the receipt to the
//! entry by id, so an auditor can verify the joule count signed in
//! one standard matches the joule count priced in the other.
//!
//! Three standards, three artifacts, one pipeline — composing at the
//! WIRE LAYER (serde_json), not at the crate-graph layer. Standards
//! that need to live in different language ecosystems compose this
//! way by design: each standard owns its bytes-on-the-wire contract,
//! and consumers compose by parsing.
//!
//! ## Why no `op-core` import
//!
//! OpenPay lives in a sibling Cargo workspace; cross-workspace path
//! deps don't carry `workspace.package` inheritance, so a direct
//! import would fail to build. The deeper reason to NOT import is
//! that this demo proves the *wire-format composition* — the
//! settlement entry it emits is consumable by the real
//! `op-ledger::Entry` deserializer (or any other ledger that
//! implements OpenPay's Entry schema).
//!
//! Run:
//! cargo run -p jouleclaw-stack --example openpay_per_joule_billing
//!
//! Output goes to stdout (the three artifacts) and to
//! `/tmp/openpay-per-joule-{receipt,entry,binding}.json`.
use ed25519_dalek::SigningKey;
use jouleclaw_cascade::tier::Runtime;
use jouleclaw_cascade::types::{ContextRef, JouleBudget, QualityFloor, Query, QueryInput};
use jouleclaw_jcr1::{TEST_KEYS_JSON, cose_sign1_envelope, to_jcr1_wire};
use jouleclaw_prov::{CascadeTier, ReceiptBuilder, ToolTouch, input_hash};
use jouleclaw_stack::JouleClawStack;
use serde_json::json;
use std::fs;
const CLOSED_AT: &str = "2026-06-04T13:00:00Z";
/// Deterministic price function: 1 cent per 100 mJ consumed
/// (i.e. $0.0001 per joule, $0.10 per kJ). A merchant sets this
/// once and operators see it in the published rate card.
///
/// Returned as USD minor units (cents) so the result drops straight
/// into OpenPay's `Money { minor_units, currency }` shape.
fn price_joules_uj_in_usd_cents(joules_uj: u64) -> i64 {
// 1 cent = 100 mJ = 100_000 µJ.
(joules_uj / 100_000) as i64
}
fn make_query(text: &str) -> Query {
Query {
input: QueryInput::Text(text.to_string()),
budget: JouleBudget::expensive(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
}
}
fn cascade_walk(runtime: &mut Runtime, question: &str) -> (String, f64) {
let answer = runtime
.answer(make_query(question))
.expect("cascade resolves");
(answer.tier_used.wire_tag().to_string(), answer.joules_spent)
}
fn main() {
println!("== 1. JouleClaw cascade walk ==");
let mut stack = JouleClawStack::with_defaults();
let question = "list my ec2 instances in us-east-1";
let (tier_a, joules_a) = cascade_walk(&mut stack.runtime, question);
let (tier_b, joules_b) = cascade_walk(&mut stack.runtime, question);
let total_joules_f = joules_a + joules_b;
let total_joules_uj = (total_joules_f * 1_000_000.0).round().max(1.0) as u64;
println!(" first ask: tier={tier_a:<6} joules={joules_a:.9}");
println!(" second ask: tier={tier_b:<6} joules={joules_b:.9}");
println!(" session total: {total_joules_uj} µJ\n");
println!("== 2. Mint a JCR-1 receipt ==");
let keys: serde_json::Value =
serde_json::from_str(TEST_KEYS_JSON).expect("test-keys.json parses");
let seed_hex = keys["ed25519_seed_hex"].as_str().expect("seed");
let seed: [u8; 32] = hex::decode(seed_hex)
.expect("hex")
.try_into()
.expect("32 bytes");
let sk = SigningKey::from_bytes(&seed);
let steward = keys["steward_kid"].as_str().expect("steward");
let tool_id = "jouleclaw:cascade.demo";
let receipt = ReceiptBuilder::new()
.input_hash(&input_hash(question))
.tier(CascadeTier::L0Cache)
.steward(steward)
.account_tool(ToolTouch {
tool_id: tool_id.into(),
joules_uj: total_joules_uj,
energy_provenance: jouleclaw_energy::Provenance::Estimator,
})
.seal()
.expect("seal");
let mut wire = to_jcr1_wire(&receipt).expect("project");
let receipt_id = "rcpt-billing-0001";
{
let obj = wire.as_object_mut().unwrap();
obj.insert("id".into(), json!(receipt_id));
obj.insert("closed_at".into(), json!(CLOSED_AT));
}
let core = jouleclaw_jcr1::signed_core_bytes(&wire).expect("core");
let envelope = cose_sign1_envelope(&core, &sk, steward);
let receipt_doc = json!({
"spec": "JCR-1",
"receipt": wire,
"signed_core_cbor_hex": hex::encode(&core),
"cose_sign1_hex": hex::encode(&envelope),
});
println!(" receipt id: {receipt_id}");
println!(" signed core: {} bytes", core.len());
println!(" COSE_Sign1: {} bytes\n", envelope.len());
println!("== 3. Apply pricing → OpenPay settlement entry ==");
let cents = price_joules_uj_in_usd_cents(total_joules_uj);
println!(" price function: 1 ¢ per 100 mJ (= $0.0001 / J)");
println!(" joules_uj: {total_joules_uj}");
println!(" USD minor: {cents} (¢{cents}, = ${:.4})\n", cents as f64 / 100.0);
// OpenPay-shape settlement transaction. Field set matches
// op-ledger v1's serde-derived `Transaction` (id, ledger_id,
// status, external_id, description, effective_at_unix_secs,
// entries: Vec<Entry>, metadata). Each Entry matches op-ledger
// v1's serde shape: account_id (newtype around Uuid → serializes
// as the bare Uuid string), direction (PascalCase enum),
// amount: Money { minor_units, currency: { code: [u8; 3],
// exponent: u8 } }. A real op-ledger ingester reads this via
// serde_json::from_value::<Transaction>.
let txn_uuid = "11111111-1111-1111-1111-000000000001";
let ledger_uuid = "00000000-0000-0000-0000-00000000ledg";
let merchant_account = "00000000-0000-0000-0000-000000000001";
let customer_account = "00000000-0000-0000-0000-000000000002";
let usd_currency = json!({
"code": [b'U' as u32, b'S' as u32, b'D' as u32],
"exponent": 2
});
let entry_doc = json!({
"spec": "OpenPay/Ledger v1 — Transaction",
"id": txn_uuid,
"ledger_id": ledger_uuid,
"status": "Posted",
"external_id": receipt_id,
"description": format!(
"JouleClaw cascade — {total_joules_uj} µJ consumed (receipt {receipt_id})"
),
"effective_at_unix_secs": 1_780_000_000u64,
"entries": [
{
"account_id": customer_account,
"amount": {"minor_units": cents, "currency": usd_currency.clone()},
"direction": "Debit"
},
{
"account_id": merchant_account,
"amount": {"minor_units": cents, "currency": usd_currency.clone()},
"direction": "Credit"
}
],
"metadata": [
["jcr1_receipt_id", receipt_id],
["joules_uj", &total_joules_uj.to_string()]
]
});
println!("== 4. Binding document — JCR-1 ↔ OpenPay ==");
let binding = json!({
"spec": "JouleClaw cross-standard binding v1",
"receipt": {
"spec": "JCR-1",
"id": receipt_id,
"joules_uj": total_joules_uj,
"signed_by_steward": steward,
},
"settlement": {
"spec": "OpenPay/Ledger v1",
"transaction_id": txn_uuid,
"amount_minor_units": cents,
"currency": "USD",
},
"price_function": {
"kind": "linear_joules",
"cents_per_microjoule_inverse": 100_000,
"documentation": "1 USD cent = 100 mJ consumed (= $0.0001 / J)",
},
"verifiable_invariant": "binding.receipt.joules_uj × cents_per_microjoule_inverse⁻¹ == binding.settlement.amount_minor_units (integer division)",
});
// Write artifacts.
fs::write(
"/tmp/openpay-per-joule-receipt.json",
serde_json::to_string_pretty(&receipt_doc).unwrap(),
)
.expect("write receipt");
fs::write(
"/tmp/openpay-per-joule-entry.json",
serde_json::to_string_pretty(&entry_doc).unwrap(),
)
.expect("write entry");
fs::write(
"/tmp/openpay-per-joule-binding.json",
serde_json::to_string_pretty(&binding).unwrap(),
)
.expect("write binding");
println!(" ✓ wrote /tmp/openpay-per-joule-receipt.json");
println!(" ✓ wrote /tmp/openpay-per-joule-entry.json");
println!(" ✓ wrote /tmp/openpay-per-joule-binding.json");
println!();
println!("Cross-standard composition: three artifacts, three specs, one pipeline.");
println!("Each artifact stands alone under its standard. Together they bind a");
println!("signed energy measurement (JCR-1) to a settlement entry (OpenPay) by");
println!("a documented, auditable price function.");
// Independent self-check: the binding's invariant holds.
let derived_cents = (total_joules_uj / 100_000) as i64;
assert_eq!(
derived_cents, cents,
"binding invariant violated: derived cents ≠ entry cents"
);
}