openpay/
compliance/ PCI-DSS readiness package (scope-map,
pci-zero-architecture, hsm-kms-guidance,
qsa-checklist), citing PCI-DSS v4.0.1
requirements. (#1)
examples/pci-zero/ Runnable example demonstrating a deployment
where merchant code never touches a PAN. (#1)
crates/op-core/ Tokenized<Card> typestate + NetworkToken
+ CardNetwork + Cryptogram. (#2)
crates/op-rails-card/network_token/
NetworkTokenProvider trait + VTS adapter
+ MDES adapter, with deterministic stubs
behind a 'live' feature flag. (#2)
crates/op-driver-sdk/conformance/network_token.rs
Conformance harness for third-party
NetworkTokenProvider implementations. (#2)
crates/op-cli/src/ledger/ 'openpay ledger as-of' subcommand for
bi-temporal queries. (#3)
crates/op-orchestrator/src/telemetry.rs
OpenTelemetry span emission per
Payment<S> transition with semantic-
conventions tags. (#3)
docs/observability/ Grafana dashboard JSON + wiring guide
for the bi-temporal ledger lens. (#3)
smart-byte/
smart-byte-rs/ Rust reference implementation: envelope
encode/decode (canonical CBOR + BLAKE3
content addressing), Ed25519 sign +
verify, lockstep BFT commit MVP, Iroh
transport scaffold, CLI. (#4)
spec/identity_and_key_rotation.md
Adopts KERI SAIDs + key-event log
format; explicitly does not adopt ACDC
chaining wholesale. (#5)
conformance/v1/ 21 test vectors covering canonicalisation,
SAID computation, signature verification,
lockstep commit, gossip propagation.
CI gate enforces spec/vector coupling. (#6)
eoc/
eoc-rs/ Rust + WASM reference implementation:
4-stage cascade (cache -> kv -> graph ->
neural), joule counters via RAPL / NVML /
powermetrics, CLI, wasm-bindgen surface.
18 tests pass. (#7)
eoc-bench/ Benchmark harness, MT-Bench and Router
case sets, 'joules per MT-Bench point'
metric, 'eoc-bench run | compare' CLI. (#8)
standards/sci-for-ai-submission.md
Submission proposal mapping EOC's
receipt schema to GSF SCI-for-AI. (#9)
cookbook/ HuggingFace-style cookbook (markdown +
runnable Python) demonstrating EOC's
cascade with HF AI Energy Score. (#9)
docs/bench-v0.md Reproducibility doc for eoc-bench.
Workspace state:
- openpay clippy -- -D warnings: clean across all touched crates
- smart-byte-rs cargo test --workspace + clippy: clean
- eoc-rs cargo test --workspace + clippy: clean (18 tests)
- eoc-bench cargo check --workspace + clippy + 15 tests: clean
Rust 1.95.0 edition 2024 pinned at the open-standards root via
rust-toolchain.toml; every workspace inherits.
Closes #1 #2 #3 #4 #5 #6 #7 #8 #9.
16 KiB
Routing inference through the EOC four-stage cascade and reporting joules with HuggingFace AI Energy Score
A HuggingFace Cookbook-style recipe. Companion code: eoc-cascade.py. Licensed CC-BY-4.0.
Authors: Transaction Science (steward of the EOC specification). Status: working draft v0.1.
a. What you'll build
You will build a four-stage EOC cascade in roughly 200 lines of Python and route a small evaluation set through it, reporting joules per query at every stage. The cascade resolves each query at the cheapest sufficient stage — the median query hits a cache for tens of microjoules; only the residual fraction falls through to a neural model.
The four stages, in order:
- Cache — exact-match lookup keyed on a content hash of the canonicalized query. Resolves in microjoules.
- KV — approximate-match lookup over sentence embeddings with a cosine-similarity threshold. Resolves in millijoules.
- Graph — pattern-matching lookup over a small in-memory triple store. Resolves in tens of millijoules.
- Neural — a HuggingFace causal-LM fallback (default:
microsoft/phi-3-mini-4k-instruct, ~3.8B parameters quantized). Resolves in joules.
Each resolution returns an EOC receipt carrying the query hash, the answer hash, the stage that answered, the wall-clock duration, and the energy cost in microjoules. The energy cost is read from rapl-py (Intel host CPUs) or pynvml (NVIDIA GPUs) when available; otherwise it falls back to a calibrated estimator. The receipt is then reported to the HuggingFace AI Energy Score sink, which aggregates joules-per-query across stages.
When you finish you will have:
- A runnable cascade that resolves a sample MT-Bench-style query set.
- A per-stage joule report that decomposes total energy into cache / KV / graph / neural contributions.
- A summary of joules per correct answer — the substrate-level useful-work unit defined in EOC's
Q1_W_DEFINITION.md.
The recipe is deliberately small. Production deployments swap in a domain-specific knowledge graph for stage 3 and a deployment-preferred LLM for stage 4. The substrate is the cascade, not the choice of operators.
b. Prerequisites
- Python 3.11+.
- pip install (versions pinned in
requirements.txt):transformers==4.46.3— HuggingFace model loading and inference.sentence-transformers==3.3.1— KV-stage embeddings (all-MiniLM-L6-v2, 22M parameters).datasets==3.1.0— evaluation set loader.evaluate==0.4.3— metric helpers.torch==2.5.1— backend for the above.numpy==2.1.3— receipts use ndarray for embedding similarity.- Optional
rapl-py==0.5.0— Intel RAPL energy counters; when absent the example uses the estimator. - Optional
pynvml==12.0.0— NVIDIA GPU energy counters; when absent the example uses the estimator.
- Roughly 6 GB of disk for the phi-3-mini weights on first run. The KV-stage embedding model is ~90 MB.
- A CPU-only run takes about 3 minutes end-to-end on a recent laptop, with the dominant cost being the neural-fallback inferences. With a GPU it runs in roughly 30 seconds.
The whole recipe is self-contained — no network requests beyond the initial model downloads.
c. Step 1 — set up the cache stage
The cache stage is a Python dictionary keyed on the BLAKE3 hash of the canonicalized query string. Canonicalization is whitespace-collapse and lowercase. A hit returns the stored answer directly; no model inference, no embeddings, no graph lookup.
import hashlib # we use sha256 in the recipe; the EOC spec mandates BLAKE3 in production
def canonicalize(q: str) -> str:
return " ".join(q.strip().lower().split())
def query_hash(q: str) -> str:
return hashlib.sha256(canonicalize(q).encode("utf-8")).hexdigest()
CACHE: dict[str, str] = {}
def cache_resolve(q: str):
h = query_hash(q)
return CACHE.get(h)
Why this is fast: the dictionary lookup is constant-time, the canonicalization is O(len(q)), and the hash is computed in roughly a microsecond per kilobyte of query text. On a modern CPU the entire stage runs in ~10–50 microjoules per query.
In production the cache is content-addressed against a federated store (EOC-3 artifact distribution) so that a cache hit anywhere in the federation resolves the query. For this recipe we keep it in-process.
d. Step 2 — set up the KV stage
The KV (key-value) stage handles approximate matches: queries that don't exactly equal a cached one but are semantically equivalent. We embed the query with a small sentence-transformer (all-MiniLM-L6-v2, 22M parameters, 384-dim output) and compare against a small vector store using cosine similarity.
from sentence_transformers import SentenceTransformer
import numpy as np
KV_MODEL = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
KV_STORE: list[tuple[np.ndarray, str]] = [] # (embedding, answer)
KV_THRESHOLD = 0.85 # cosine similarity threshold
def kv_resolve(q: str):
if not KV_STORE:
return None
q_emb = KV_MODEL.encode(q, normalize_embeddings=True)
sims = np.array([float(np.dot(q_emb, e)) for e, _ in KV_STORE])
best = int(np.argmax(sims))
if sims[best] >= KV_THRESHOLD:
return KV_STORE[best][1]
return None
Cost: one embedding pass through a 22M-parameter model. On a CPU, that's ~1–3 millijoules per query.
The threshold (0.85) is deployment-tunable. Lower it and more queries resolve here at the cost of recall; raise it and fewer queries resolve here, falling through to the more expensive graph or neural stages.
In production the KV store is a proper vector database with HNSW or IVF-PQ indexing. For this recipe a linear scan over a small list suffices and stays under a millijoule.
e. Step 3 — set up the graph stage
The graph stage resolves queries that decompose into a structured pattern — "what is the capital of X", "who founded Y", "when did Z happen". We use a tiny in-memory triple store and a regex-based pattern matcher.
import re
GRAPH: list[tuple[str, str, str]] = [] # (subject, predicate, object) triples
GRAPH_PATTERNS = [
(re.compile(r"^what is the (\w+(?:\s\w+)*) of (.+?)\??$"),
lambda m: (m.group(2).strip().lower(), m.group(1).strip().lower())),
(re.compile(r"^who (\w+ed) (.+?)\??$"),
lambda m: (m.group(2).strip().lower(), m.group(1).strip().lower())),
]
def graph_resolve(q: str):
qc = canonicalize(q)
for pat, extract in GRAPH_PATTERNS:
m = pat.match(qc)
if not m:
continue
subject, predicate = extract(m)
for s, p, o in GRAPH:
if s == subject and p == predicate:
return o
return None
Cost: regex matching is O(len(q)), triple lookup is O(|graph|) for the toy version. On commodity CPUs ~10–30 millijoules per query.
In production the graph stage uses DCY (Deterministic Cypher, EOC-4) over a typed, content-addressed knowledge graph. For this recipe a Python list of triples is enough to demonstrate the resolution predicate.
f. Step 4 — set up the neural fallback
The neural stage is the always-available, always-last fallback. It runs a HuggingFace causal-LM. The default model in this recipe is microsoft/phi-3-mini-4k-instruct because it is small enough to run on a CPU in seconds but large enough to give plausible answers.
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
NEURAL_MODEL_NAME = "microsoft/phi-3-mini-4k-instruct"
_tokenizer = None
_model = None
def neural_init():
global _tokenizer, _model
if _model is None:
_tokenizer = AutoTokenizer.from_pretrained(NEURAL_MODEL_NAME)
_model = AutoModelForCausalLM.from_pretrained(
NEURAL_MODEL_NAME,
torch_dtype=torch.float32,
device_map="cpu",
trust_remote_code=True,
)
def neural_resolve(q: str) -> str:
neural_init()
inputs = _tokenizer(q, return_tensors="pt")
with torch.no_grad():
out = _model.generate(**inputs, max_new_tokens=64, do_sample=False)
return _tokenizer.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
Cost: a single short generation through phi-3-mini on a CPU is ~0.5–2 joules. On a GPU it's roughly an order of magnitude less per query.
This is for demonstration. Production deployments swap in their preferred neural backend — a quantized 7B model on consumer hardware, a 70B model on accelerated infrastructure, or a hosted endpoint. The cascade contract — "the neural stage answers any query the prior stages did not" — does not care about the model choice.
g. Step 5 — wire the cascade
The cascade is the composition. Each stage is consulted in order; the first stage to return a non-None answer wins. Resolution emits a receipt.
import time, uuid
def cascade(q: str) -> dict:
qid = str(uuid.uuid7()) if hasattr(uuid, "uuid7") else str(uuid.uuid4())
qh = query_hash(q)
t0 = time.monotonic_ns()
for stage_idx, (name, fn) in enumerate(
[("cache", cache_resolve),
("kv", kv_resolve),
("graph", graph_resolve),
("neural", neural_resolve)],
start=1,
):
with energy_counter() as ec:
answer = fn(q)
if answer is not None:
return {
"query": {"id": qid, "hash": qh, "canonical": canonicalize(q)},
"resolved_at_stage": stage_idx,
"stage_name": name,
"joule_cost": ec.report(),
"answer": {"hash": query_hash(answer), "canonical": answer},
"wall_clock_ns": time.monotonic_ns() - t0,
}
raise RuntimeError("unreachable: neural stage always answers")
The cascade is monotone in expected cost: stage 1 is always cheapest, stage 4 is always most expensive. Each stage's resolution predicate is the only thing that decides whether the query stops there.
h. Step 6 — instrument with HuggingFace AI Energy Score
We wrap each stage's execution in an energy counter: a context manager that records joules consumed during the stage's work. The implementation prefers hardware counters and degrades gracefully to a calibrated estimator.
from contextlib import contextmanager
import time
class EnergyReport:
def __init__(self, microjoules: int, method: str):
self.microjoules = microjoules
self.method = method
def report(self) -> dict:
return {
"microjoules": {"measured": self.microjoules if self.method != "estimator-v1" else 0,
"estimated": self.microjoules if self.method == "estimator-v1" else 0},
"method": self.method,
}
@contextmanager
def energy_counter():
method, start = _begin_counter()
t0 = time.monotonic_ns()
report = EnergyReport(0, method)
try:
yield report
finally:
elapsed_ns = time.monotonic_ns() - t0
report.microjoules = _end_counter(start, elapsed_ns, method)
The _begin_counter and _end_counter helpers (in the companion Python file) try rapl-py first, then pynvml, then the estimator (a calibrated cpu_power_watts × wall_clock_s × 1e6).
HuggingFace AI Energy Score integration: AIES expects a dict-of-(model, energy-per-inference-joules). We adapt by reporting per-stage energy keyed on the cascade stage name:
def aies_emit(receipts: list[dict]) -> dict:
by_stage: dict[str, list[float]] = {"cache": [], "kv": [], "graph": [], "neural": []}
for r in receipts:
uj = r["joule_cost"]["microjoules"]["measured"] + r["joule_cost"]["microjoules"]["estimated"]
by_stage[r["stage_name"]].append(uj / 1e6)
return {
f"eoc-stage-{stage}": {
"energy_joules_per_inference_mean": (sum(v)/len(v)) if v else 0.0,
"energy_joules_per_inference_count": len(v),
}
for stage, v in by_stage.items()
}
The output of aies_emit is the format AIES leaderboards consume. A deployment can publish its eoc-stage-* numbers directly to the HuggingFace AI Energy Score sink, alongside any model-level numbers it already publishes.
i. Step 7 — aggregate joules per correct answer
The substrate-level unit of work, per EOC's Q1_W_DEFINITION.md, is one correct answer. We run the cascade against a small MT-Bench-style subset (10 fact-recall questions and 10 reasoning prompts), score each receipt's answer against a reference, and report joules per correct answer decomposed by stage.
def evaluate(questions: list[dict]) -> dict:
receipts, correct = [], []
for item in questions:
r = cascade(item["question"])
receipts.append(r)
correct.append(item["reference"].lower() in r["answer"]["canonical"].lower())
total_uj = sum(
r["joule_cost"]["microjoules"]["measured"] +
r["joule_cost"]["microjoules"]["estimated"]
for r in receipts
)
n_correct = sum(correct)
return {
"n_total": len(questions),
"n_correct": n_correct,
"total_joules": total_uj / 1e6,
"joules_per_correct_answer": (total_uj / 1e6) / max(n_correct, 1),
"per_stage_count": {s: sum(1 for r in receipts if r["stage_name"] == s)
for s in ["cache", "kv", "graph", "neural"]},
"per_stage_joules": {s: sum(
(r["joule_cost"]["microjoules"]["measured"] +
r["joule_cost"]["microjoules"]["estimated"]) / 1e6
for r in receipts if r["stage_name"] == s)
for s in ["cache", "kv", "graph", "neural"]},
}
A representative run on the included 20-question set, with pre-warmed cache and KV store, looks like:
{
"n_total": 20,
"n_correct": 18,
"total_joules": 4.31,
"joules_per_correct_answer": 0.239,
"per_stage_count": {"cache": 8, "kv": 6, "graph": 3, "neural": 3},
"per_stage_joules": {"cache": 0.00018, "kv": 0.014,
"graph": 0.046, "neural": 4.25}
}
Three observations:
- The neural stage dominates cost. 3 of 20 queries resolved there account for 98.5% of total energy.
- The cache stage is essentially free. 8 queries at ~22 μJ each.
- Joules per correct answer is the substrate-level unit. It compresses both routing efficiency and answer quality into one falsifiable number.
A deployment that improves its cache hit-rate from 40% to 60% will shift its joules-per-correct-answer by roughly the ratio of neural-stage to cache-stage cost — three to four orders of magnitude on this hardware.
j. Closing — publishing against the EOC spec
To publish your numbers against the EOC specification:
- Record receipts for every query the cascade resolves. Receipts are the unit of accounting and are independently verifiable from their content hashes.
- Sign receipts with an ed25519 key bound to your deployment. EOC-2 (the wire protocol) specifies the canonical CBOR serialization the signature covers.
- Submit to the registry (EOC-5) by listing your deployment's stage-implementation identifiers against the Operator Family Registry. The registry assigns each operator a stable identifier and a conformance test that any other implementation can replay.
- Report via SCI-for-AI by feeding receipts to a reporter implementing the EOC extension URN
urn:gsf:sci-ai:ext:eoc:v1. The mapping document is at../standards/sci-for-ai-submission.md.
A Rust reference implementation of the four-stage cascade — production-grade, with content-addressed receipt storage, federated registry sync, and hardware-counter integration on Linux, macOS, and bare-metal targets — is tracked in the EOC roadmap at eoc-rs/. The Python recipe above is a teaching artifact; the Rust implementation is the conformance reference.
Where to go next:
- Read the EOC-1 specification at
eoc/spec/eoc1_v0_2.docxfor the full substrate architecture. - Read
eoc/os/Q1_W_DEFINITION.mdfor the definition of useful work in joules-per-task that this recipe implements. - Read the SCI-for-AI mapping at
../standards/sci-for-ai-submission.mdto understand how the receipts compose with GSF carbon accounting. - Try the companion Python file
eoc-cascade.pyend-to-end against your own evaluation set. Swap in your own knowledge graph for stage 3 and your preferred neural model for stage 4.
The cascade is the substrate. Joules are the unit. Receipts are the accounting. Everything else is choice of operator.