meters
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/meters on onyx-1 testnet
realmapplicationbillinglive on onyx-1
Bill for metered usage, with discounts as volume grows.
Usage is recorded as it is served and each period settles from a prepaid deposit, so neither side has to trust an invoice.
In detail curated
USAGE-BASED BILLING WITH GRADUATED MARGINAL PRICING -- the usage-based complement to r/subscriptions' flat recurring plans and the one billing shape this portfolio lacked (the finding of its own discovery gate, which took candidate E after the receivables and payslips gates deferred it twice as 'a larger product than a primitive-consumer'). A provider publishes a METER: a unit of service, an ugnot list price per unit, a billing period in blocks, and a p/tiers schedule of marginal volume discounts over the ugnot BASE (units x unitPrice). A consumer ENROLLS and prepays a deposit; the provider RECORDS usage as it serves it; each period's accumulated usage settles lazily from the consumer's deposit into the provider's claimable balance, the protocol fee splitting at settlement from the enrollment's snapshotted bps. tiers' rate cap makes Charge(base) <= base an ARITHMETIC FACT, so a schedule discounts list and can never surcharge it; a flat price is the one-rate degenerate schedule and a free allowance is a 0-rate first tier, both first-class. THREE TRUST STATEMENTS GOVERN IT. (1) USAGE IS PROVIDER-ASSERTED AND THE DECLARATION IS THE TRUST BOUNDARY -- the boundary r/books draws at owner-posted entries and r/cashflow at Policy.Cash: the realm cannot verify off-chain consumption, it makes the assertion attributable (Record is provider-only, against an enrollment to that provider's meter), bounded, and auditable (every unit is an event and a readable row). (2) LIABILITY IS BOUNDED BY DEPOSIT, FAIL-CLOSED: Record REFUSES -- never clips -- any posting whose end-of-period charge would exceed the deposited balance, WithdrawDeposit holds back the pending charge, and no arrears exist anywhere, so a consumer's maximum exposure to a hostile or buggy provider is exactly what they deposited, by construction. (3) SETTLEMENT IS LAZY AND EXACT: usage belongs to the one period it was recorded in, the first touch after a boundary settles it, silent periods owe zero, and a permissionless Settle valve means neither party's absence wedges the other's funds. Enrollment terms (price, schedule, period, fee bps) are COPIED AT ENROLL and never re-read; meters are immutable, so nothing a consumer agreed to can move underneath them. Funds move on pull paths only through the coinio guard-trio over a feeledger whose conservation invariant (Held == Liabilities == deposits + claimables + fee pot) was checked LIVE at a nonzero balance. THE AUDIT'S RED IS THE DESIGN'S BEST SENTENCE: the zero-charge settlement (a free tier's, or a positive rate floor-rounding to zero) originally called MustWithdraw(0), which feeledger refuses, wedging the enrollment and stranding the deposit forever through every funds path -- fixed by guarding the funds move, never by refusing free usage, both routes pinned by tests a fresh-context adversarial pass proved non-vacuous, and the zero-charge Exit was then exercised ON CHAIN with a full refund.
Use it
A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.
Read it — no key, no account, no GNOT
gnokey query vm/qeval \
--data 'gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/meters.FeeBps()' \
--remote https://rpc.onyx.testnets.gno.land:443FeeBps is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.
Change its state
Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 16 crossing functions, with the argument names filled in.
Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.
Import it from another realm
import "gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/meters"The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.
Deployments
onyx-1 testnet onyx-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/meters |
|---|---|
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Deployed at height | 481,166 |
| Deploy transaction | f861becf4ca8cb467607f629f1bc9289923d4e0271b4c3425d3df054a3b9ec9d look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 45,913,168 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml meters.gno |
| Deployed bytes | meters.gno — 33,970 bytes |
| sha256 | a7ee325b90367f78137695efe142bafbb0a52c062d4b34fc476710d8bef5a49e |
| Realm address | g1upuageltru7n0gzj3txmc8qm0wzpcj76jchvq7 |
Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:
curl -sS 'https://onyx.testnets.gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/meters$download&file=meters.gno' | shasum -a 256Expected: a7ee325b90367f78137695efe142bafbb0a52c062d4b34fc476710d8bef5a49e — 33,970 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 33 exported functions.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AcceptAdminActiveEnrollIDBalanceOfChargeHeadroomOfClaimClaimAllClaimableOfCreateMeterCurrentPeriodOfDepositEnrollExitFeeBpsFeesAccruedHeightHeldLiabilitiesNumEnrollmentsNumMetersPendingChargeOfPriceOfRecordRenderRetireMeterScheduleOfSetFeeBpsSetFeeRecipientSettleSweepDenomTransferAdminUsersTotalWithdrawDepositWithdrawFees
Overview
Realm meters is usage-based billing with graduated marginal pricing: a provider publishes a METER (a unit of service, a list price per unit, a billing period, and a tiers.Schedule of marginal volume discounts), a consumer ENROLLS and prepays a deposit, the provider RECORDS usage as it is served, and each period's accumulated usage settles from the consumer's deposit into the provider's claimable balance.
It is the usage-based complement to r/subscriptions' flat recurring plans — the one billing shape this portfolio lacked (its discovery gate's whole finding). The pricing identity is p/tiers':
base = units × unitPrice (checked int64 multiplication) due = Schedule.Charge(base) (≤ base — marginal discount off list)
tiers validates every rate into [0, RateScale], so Charge(base) ≤ base is an arithmetic fact: a schedule can DISCOUNT list price and can never surcharge it. A flat price is the one-rate schedule (no edges, rates "10000") — the degenerate case, not a second code path.
The three trust statements, in order of who bears what
- USAGE IS PROVIDER-ASSERTED, and that declaration is the trust boundary — the same boundary r/books draws at owner-posted entries and r/cashflow at Policy.Cash. The realm cannot verify off-chain consumption; what it does is make the assertion attributable (only the meter's provider may Record, and only against an enrollment to that meter), bounded (next statement), and auditable (every unit is an event and a readable row).
- LIABILITY IS BOUNDED BY DEPOSIT, fail-closed. Record refuses any posting whose end-of-period charge would exceed the enrollment's deposited balance, and a withdrawal leaves the pending charge covered. Charge is monotone in units (tiers' documented monotonicity), so the refusal predicate is well-defined. No arrears exist anywhere in this design: the realm never holds a claim against funds a consumer has not deposited.
- SETTLEMENT IS LAZY AND EXACT. Usage accumulates against the period it was recorded in; the first touch after a period boundary settles that period's charge. A period nobody recorded usage in owes zero, so lazy settlement loses nothing — there is no keeper, no upkeep dependency, and a Settle valve is permissionless so neither party's absence can wedge the other's funds.
Funds move on pull paths only: providers Claim, consumers WithdrawDeposit or Exit, the fee recipient WithdrawFees. Periods are fixed-length block windows anchored at Enroll height — runtime.ChainHeight() throughout, never a caller-supplied time (r/books' obligation, inherited verbatim).
REALM-CALLER CAVEAT, inherited from the siblings verbatim: coinio's receipt guard admits only EOA payers, so enrolling consumers are EOAs; assertNoSend reads the ORIGIN envelope, so every non-payable function refuses any transaction that attached coins anywhere. Providers may be EOAs or realms, but a realm provider must expose its own crossing path to Claim, or what it earns is stranded.
Imports
chainchain/runtimechain/runtime/unsafegno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coiniogno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledgergno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiersgno.land/p/nt/avl/v0gno.land/p/nt/markdown/sanitize/v0strconv
Constants and variables
Meter status values.
const (
MeterActive = "active"
MeterRetired = "retired"
)
Enrollment status values. Exited is terminal.
const (
EnrollActive = "active"
EnrollExited = "exited"
)
Input bounds.
const (
MaxTitleLen = 80
MaxUnitLen = 24
// MinUnitPrice keeps a meter from being free-by-rounding; a provider
// who wants a free tier declares a 0-rate first tier instead, which
// keeps the arithmetic visible in the schedule rather than implicit
// in a zero price.
MinUnitPrice = int64(1)
// MaxUnitPrice keeps base = units × unitPrice far from int64
// overflow for any usage a deposit could plausibly cover; the
// checked multiplication is still there and still refuses.
MaxUnitPrice = int64(1_000_000_000) // 1000 GNOT per unit
// MinPeriodBlocks/MaxPeriodBlocks bound a meter's billing period —
// the same bounds and reasoning as r/subscriptions: the floor keeps
// settlement from becoming a per-block treadmill, the ceiling keeps
// period arithmetic far from overflow.
MinPeriodBlocks = int64(10)
MaxPeriodBlocks = int64(7500000)
// MaxMetersPerProvider bounds catalog monopolization by a single
// address — the finding that was RED in permission_registry and
// service_registry, carried from the start here.
MaxMetersPerProvider = 20
// MaxEnrollmentsPerConsumer bounds one account's open-enrollment
// state. Exited enrollments free their slot.
MaxEnrollmentsPerConsumer = 100
// RenderLimit bounds every rendered list — an unbounded Render was
// YELLOW in three prior audits.
RenderLimit = 20
)
Denom is the only asset this realm accepts.
const Denom = "ugnot"
MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.
const MaxFeeBps = int64(1000)
Functions
AcceptAdmin
func AcceptAdmin(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptAdmin(cross(cur), ...).
AcceptAdmin completes a staged admin transfer. Pending admin only.
ActiveEnrollID
func ActiveEnrollID(meterID int64, consumer address) (int64, bool)
ActiveEnrollID resolves the caller-free (meter, consumer) index.
BalanceOf
func BalanceOf(enrollID int64) int64
BalanceOf is an enrollment's deposited balance as the LEDGER holds it — including a not-yet-settled elapsed period's charge; net view: subtract PendingChargeOf.
ChargeHeadroomOf
func ChargeHeadroomOf(enrollID int64) int64
ChargeHeadroomOf is how much MORE charge the current period could absorb before Record's deposit bound refuses: balance − pending charge.
Claim
func Claim(cur realm, amount int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asClaim(cross(cur), ...).
Claim sends amount ugnot of the caller's claimable balance back to the caller. Providers earn into this balance at every settlement.
ClaimAll
func ClaimAll(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asClaimAll(cross(cur), ...).
ClaimAll sends the caller's entire claimable balance back to the caller. Fails if there is nothing to claim.
ClaimableOf
func ClaimableOf(a address) int64
CreateMeter
func CreateMeter(cur realm, title, unit string, unitPrice, periodBlocks int64, edges, rates string, maxFeeBps int64) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asCreateMeter(cross(cur), ...).
CreateMeter publishes a meter and returns its id. No coins are accepted; the storage deposit the caller pays is the anti-spam. edges and rates are comma-separated int64 lists building the tiers.Schedule over the ugnot BASE (units × unitPrice): len(rates) == len(edges)+1, each rate in [0, 10000] ten-thousandths of list price. "" edges with rates "10000" is a flat meter. The current protocol fee is snapshotted in and must not exceed maxFeeBps, the ceiling the provider signed for — pass MaxFeeBps to accept any legal fee. Meters are immutable once created: price, period or schedule changes are a new meter, so nothing a consumer agreed to can move underneath them.
CurrentPeriodOf
func CurrentPeriodOf(enrollID int64) int64
CurrentPeriodOf is the period index now open for an enrollment.
Deposit
func Deposit(cur realm, enrollID int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asDeposit(cross(cur), ...).
Deposit adds the attached coins to the caller's own enrollment deposit. Elapsed usage settles first, so a top-up lands on a current balance.
Enroll
func Enroll(cur realm, meterID int64) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asEnroll(cross(cur), ...).
Enroll opens an enrollment on a meter for the caller, with any attached coins credited as the initial deposit. One active enrollment per (meter, consumer); Exit frees the slot. Terms (price, schedule, period, fee) are copied in at this moment and never re-read.
Exit
func Exit(cur realm, enrollID int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asExit(cross(cur), ...).
Exit closes the caller's own enrollment: elapsed periods settle, the CURRENT period's recorded usage settles immediately (the provider served it; exiting is not an escape from it), the remaining deposit refunds in full, and the slot frees. Terminal.
FeeBps
func FeeBps() int64
FeesAccrued
func FeesAccrued() int64
Height
func Height() int64
Held
func Held() int64
Liabilities
func Liabilities() int64
NumEnrollments
func NumEnrollments() int64
NumMeters
func NumMeters() int64
PendingChargeOf
func PendingChargeOf(enrollID int64) int64
PendingChargeOf is the charge the enrollment's current recorded usage would settle at — what WithdrawDeposit holds back.
PriceOf
func PriceOf(meterID, units int64) int64
PriceOf prices units of meter meterID at its schedule: the charge a period closing at exactly that usage would settle. The quote every consumer can take before enrolling and every provider can show beside an invoice.
Record
func Record(cur realm, enrollID, units int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRecord(cross(cur), ...).
Record asserts that the enrollment's consumer used units more units of the meter's service in the CURRENT period. Provider only. Fail-closed on the deposit bound: if charging the period's new total at the enrollment's schedule would exceed the deposited balance, the whole posting is refused — never clipped, because a clipped assertion is a wrong assertion with the same signature as a right one. Any elapsed period settles first, so the bound is always measured against the balance that will actually back this period.
Render
func Render(path string) string
Render shows the catalog at "", a meter at "meter/<id>", an enrollment at "enroll/<id>". Every user string is sanitized; every list is bounded by RenderLimit with the bound stated.
RetireMeter
func RetireMeter(cur realm, meterID int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRetireMeter(cross(cur), ...).
RetireMeter stops NEW enrollments and NEW usage on the caller's own meter. Existing enrollments keep their funds paths whole: pending usage still settles, deposits stay withdrawable, Exit still refunds. The provider's meter slot frees. Terminal.
ScheduleOf
func ScheduleOf(meterID int64) (edges, rates []int64)
ScheduleOf returns a meter's schedule as its edge and rate slices.
SetFeeBps
func SetFeeBps(cur realm, bps int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSetFeeBps(cross(cur), ...).
SetFeeBps sets the protocol fee snapshotted into NEW meters. Admin only. No existing meter or enrollment moves.
SetFeeRecipient
func SetFeeRecipient(cur realm, recipient address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSetFeeRecipient(cross(cur), ...).
SetFeeRecipient points fee withdrawal and sweeps at recipient. Admin only.
Settle
func Settle(cur realm, enrollID int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSettle(cross(cur), ...).
Settle settles any fully-elapsed period of an enrollment. Permissionless by design — like the sibling realms' valves, no settlement's liveness may depend on either party showing up. No coins may ride on it, and calling it on an enrollment with nothing elapsed is a refused no-op so the event stream never carries empty settlements.
SweepDenom
func SweepDenom(cur realm, denom string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSweepDenom(cross(cur), ...).
SweepDenom recovers out-of-band coins (sent by raw bank transfer, outside any entrypoint) to the fee recipient. For the ledger denom the reserve is Liabilities() — user balances and the fee pot are structurally unreachable. Fee recipient only.
TransferAdmin
func TransferAdmin(cur realm, successor address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asTransferAdmin(cross(cur), ...).
TransferAdmin stages successor as the next admin; AcceptAdmin completes it. Two-step with consent — a one-step transfer bricked resources in two prior audits (service_registry Y4, permission_registry Y4).
UsersTotal
func UsersTotal() int64
WithdrawDeposit
func WithdrawDeposit(cur realm, enrollID, amount int64)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asWithdrawDeposit(cross(cur), ...).
WithdrawDeposit returns amount of the caller's own enrollment deposit. The current period's pending charge stays covered: withdrawable is balance − Charge(current usage), so a provider's already-recorded usage can never be defunded. Elapsed usage settles first.
WithdrawFees
func WithdrawFees(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asWithdrawFees(cross(cur), ...).
WithdrawFees sends the accrued fee pot to the fee recipient. Only the fee recipient may call it, and only the pot moves.
Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.
Dependencies
chain-attested| Imports | chain, chain/runtime, chain/runtime/unsafe, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiers, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv |
|---|---|
| First-party dependencies | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiers |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- A PROVIDER INSIDE THE DEPOSIT BOUND CAN CHARGE FOR SERVICE IT DID NOT RENDER. The realm bounds and attributes the assertion; it cannot adjudicate it. This is the product's defining limitation, stated in the package doc's first trust statement rather than discovered by a reader.
- A ZERO-CHARGE PERIOD IS INVISIBLE IN THE Settled EVENT STREAM BY DESIGN -- the stream carries funds moves; an indexer reconstructing period closures needs UsageRecorded too.
- CONSUMERS ARE EOAs (coinio's receipt guard); a REALM may enroll coinless but can never fund the enrollment -- inert, not unsafe. Providers may be realms but must expose their own crossing path to Claim or earnings strand.
- RetireMeter STOPS NEW ENROLLMENTS AND NEW USAGE; IT DOES NOT REFUND. Consumers exit themselves, and their funds paths stay whole after retirement -- exercised by test, not live.
- THE LIVE LIFECYCLE RAN UNDER A ONE-KEY HARNESS, so provider, consumer, admin and fee recipient were the same address on chain. Every cross-party refusal (Record by a non-provider, withdraw by a non-consumer, fee paths by a stranger) is proven by the committed suite and the 39-case sabotage sweep, not by a live transaction -- the same single-key limitation netting and invoices record.
- NO MULTI-PERIOD LIVE SETTLEMENT RAN: both live enrollments were exited inside their first period (the partial-period settle path). The rollover settle is proven by test (TestSettlementMovesChargeWithFee, TestEmptyPeriodsOweZero) and by the permissionless-valve case; on chain it awaits an enrollment that outlives a period.
- storage_bytes and storage_deposit are null for the reason in storage_measurement_note -- the figures ARE chain-attested (63,647 B / 6,364,700ugnot at height 481170) and are withheld from these fields only because verify_catalog C2's one-block window cannot see an event four blocks later. Do not read the nulls as an unmeasured deploy.
- SetFeeRecipient/TransferAdmin accept non-canonical (uppercase) address spellings that IsValid() tolerates but no live caller can match, wedging WithdrawFees/SweepDenom until re-set (recoverable: admin re-runs the setter; AcceptAdmin's consent step keeps the admin role itself safe). Successor: the canonical-lowercase guard invoices already carries. Audit 2026-11-04, disclosed per the operator's disclose-and-queue-successor rule; details and severity in [an unpublished record].
Source
| Source file | onyx/r/meters/meters.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository) |
|---|---|
| Matches the deployed bytes | yes — byte-identical |