Overview
Normal usage billing asks the buyer to trust how the vendor defines a unit, counts usage, treats invalid output, and calculates the final invoice. The buyer receives one number and has no way to check it.
Ceiling precommits five things before processing begins: the unit definition, the deterministic validator, the failure policy, the unit price, and the maximum number of units. After processing, it publishes the output and billing metadata needed to rerun the same validator and recompute the charge.
A Ceiling invoice is derived from a published session artifact rather than accepted as an opaque vendor total.
Mechanism
- .001 Commit
Canonicalize the policy, hash it with Keccak-256, and record the hash on-chain before any output exists.
- .002 Authorize
The buyer authorizes no more than
maxUnits × unitPrice. In the reference session that is 25 × $0.002 = $0.050. No valid execution can bill more. - .003 Meter
Records are validated in order. Only accepted units are counted. The first invalid record ends the billable stream.
- .004 Settle
The actual bill is computed, the outcome is recorded, and the session artifact is published for recomputation.
Policy and validator
The policy is versioned and serialized canonically — object keys are sorted recursively so that insertion order cannot change the hash. Including maxUnits in the policy prevents the billing ceiling from being changed after the policy is accepted.
{
"failurePolicy": "cut-on-first-invalid",
"maxUnits": 25,
"schema": { "name": "string", "score": "finite-number" },
"unitDefinition": "one canonical JSON record",
"unitPriceAtomic": "2000",
"validator": "canonical-json-record-v1",
"version": 1
}The reference validator accepts a record only when name is a non-empty string and score is a finite number. Every rejection carries a machine-readable reason: record_not_object, name_not_string, name_empty, or score_not_finite.
Same input, same result, on any machine, at any time. No model calls, no randomness, no clocks, no network. If the buyer cannot reproduce it, the whole guarantee collapses.
Metering and failure
Under cut-on-first-invalid, the first rejected record is not billed and every record after it falls outside the billable stream. The meter also slices the produced records to maxUnits before iterating, so a producer cannot exceed the committed ceiling.
record 01–17 ACCEPT
record 18 REJECT — score_not_finite
STREAM CUT
record 19–25 not chargedMoney arithmetic
Monad Testnet USDC has six decimals. Every amount is an integer in atomic units held as bigint. Floating point is never used for billing; decimal strings are display formatting only.
| Quantity | Formula | Reference session |
|---|---|---|
| Ceiling | maxUnits × unitPriceAtomic | 25 × 2,000 = 50,000 |
| Actual | acceptedUnits × unitPriceAtomic | 17 × 2,000 = 34,000 |
| Unused | ceiling − actual | 50,000 − 34,000 = 16,000 |
Recomputation
An independent party needs the published artifact, then:
- Canonicalize the policy and hash it; compare to
policyHash. - Canonicalize the full output and hash it; compare to
outputHash. - Rerun the validator in order, stopping at the first invalid record.
- Count accepted units before the cut.
- Recompute actual and unused amounts with integer arithmetic.
- Compare every result against the published values.
recomputeSession returns five booleans: policyHashMatches, outputHashMatches, acceptedUnitsMatches, actualAmountMatches, and billingMatches.
The registry can only check that settledAmount == acceptedUnits × unitPrice. A seller who inflates the unit count and keeps the arithmetic consistent still passes that check. Only rerunning the validator over the published output catches it — which is why the raw records are published, not just the totals.
On-chain registry
CeilingRegistry is a registry, not an escrow and not an output validator. It never receives raw output, never custodies payment, and never initiates a refund. It enforces:
- each
sessionIdcan be committed once; - a session must exist before an outcome is recorded;
- only the session creator can record its outcome;
- an outcome can be recorded once;
acceptedUnits ≤ maxUnits;settledAmount == acceptedUnits × unitPrice.
- POLICY HASH
0x2649acf4fe4f9f179b61b2b717d3b58299298de37eefcc8118aaafc328c3f123- OUTPUT HASH
0xc63b3f62cfe8ee0fced6f8488804b36825f42f3f251de2196bea7e8903e1f986
Payment paths
Path A — x402 upto
The intended native path. The buyer signs a maximum authorization, the server meters output, and the facilitator settles an amount less than or equal to that maximum. The unused portion never moves.
Path B — Ceiling Boomerang
The fallback. Charge the full ceiling with x402 exact, run the same deterministic pipeline, then refund the difference. It preserves the product economics but is not protocol-equivalent to upto: the full ceiling reaches the receiver first, and the refund is a separate transaction with its own funding and failure assumptions.
Integration READY. Facilitator preflight PASS. Live settlement WAITING FOR TEST USDC. No live USDC payment or refund is claimed, and no placeholder transaction is shown in its place.
Running locally
Node.js 20 or newer.
npm install
npm run check # typecheck
npm run test:core # deterministic self-check
npm run contract:compile
npm run demo:core # full session in the terminalThe website and interactive session:
cd web
npm install
npm run devPayment commands need a funded test wallet and a local .env.local. Never commit private keys, and use dedicated burner wallets only.
Limitations
- The validator is deterministic and domain-specific. It proves compliance with declared field rules, not semantic truth or output quality.
- The seeded producer exists to make the demonstration reproducible. It is not a real model call.
- The registry does not inspect raw output.
- External recomputation depends on the published artifact remaining available.
- Canonical serialization follows JSON number formatting, so a non-JavaScript reimplementation must match those rules to reproduce
outputHash. - The exact-and-refund fallback has two on-chain operations. A failed refund leaves the ceiling charge in place.