monetize
The money engine for the Vetra products: plans, entitlement as signed facts, the reserve → provision → capture transaction, plugin seams for payment vendors and cloud providers, and paid signup. One gRPC server and one operator console serve many products; a product compiles in a thin client and puts one check at its own choke point.
- Plugin seams for payment vendors and cloud providers. The paid implementations live in a separate private repository; this tree carries the traits, the engine, the server, the console, and the implementations nobody needs an account for.
- Signed entitlement facts a product verifies offline: one atomic load and a map lookup, no network call on the request path.
- Operator console (Leptos/WebAssembly), served by its own read-only backend.
- An append-only ledger written before every external call; every derived number is a fold of it.
- The same code runs https://monetize.vetra.se
Building
git clone ssh://git.gunnar.rs:2222/nordisk/monetize
git clone ssh://git.gunnar.rs:2222/nordisk/edda
git clone ssh://git.gunnar.rs:2222/nordisk/Skidbladnir
cd monetize
cargo build --release -p monetize-server -p monetize-ui-server
The root workspace takes some crates by path from the sibling checkouts ../edda and
../Skidbladnir, so both sit beside the clone: a clone of monetize alone does not
build. A product that links monetize takes its open crates (monetize-embed,
monetize-product, monetize-proto, ...) from crates.io and needs no checkout.
This page documents monetize 0.3.0 — the version monetize-server --version
prints.
Docs: Integrating an application with monetize v2.0
(the picture, the data model) · today's STE manual
· the plan: MONETIZE-V2.0-PLAN.md in workspace_nordisk.
License: AGPL-3.0-only, © 2026 Vetra Automation AB and Ignalina ApS. Commercial
terms: COMMERCIAL-LICENSING.md.
The solo monetize box
ONE machine runs monetize for every product. monetize-admin (its own
workspace, crates/monetize-admin) is that box's PID 1 on a tunnr kernel, and
the same binary is its two other processes, each on its own loopback address:
| process | started as | listens on |
|---|---|---|
| monetize-server | monetize-admin server --config /data/monetize/monetize.toml | 127.0.0.1:50061 |
| the console | monetize-admin ui --bind 127.0.0.1:8091 --server 127.0.0.1:50061 --root /data | 127.0.0.1:8091 |
| PID 1 (the edge) | korp-start, no arguments | :80/:443, the operator's door :8475 |
The edge carries /callback/<kind>/<instance> and the estate-facing gRPC
services (SoldEstate, Signup, EstateOrder, Purchase, Products) to the
server, refuses every other gRPC service (404), and carries the rest to the
console. monetize dials no estate: every estate calls monetize. Its only
outbound calls go to the cloud providers and the payment vendors.
Each product names its vendor instance. Each Stripe instance reads
MONETIZE_STRIPE_<INSTANCE>_SECRET_KEY and
MONETIZE_STRIPE_<INSTANCE>_WEBHOOK_SECRET, and its webhook is
https://<box>/callback/stripe/<instance>:
[vendors.stripe.gunnar]
enabled = true
[vendors.stripe.holger]
enabled = true
[products.gunnar]
vendor = "stripe/gunnar"
[products.holger]
vendor = "stripe/holger"
Each product sells its own signup from its own [signups.<product>]: its
plans (every id starts with <product>/), the meter their caps land on, its
oversell allowance and its signup guest (the feed's signup_guest.<product>.*).
The block's name is the product; Signup.CheckName, Signup.Start and
Signup.Plans name the product they are for, and a call that names none is
refused. The old single [signup] is refused at load.
[signups.gunnar.plans."gunnar/10gb-1m"]
amount_minor = 10000
currency = "Sek"
cap_bytes = 10737418240
period_days = 30
[signups.holger]
cap_meter = "disk_bytes"
The installer feeds the box (the kit's declaration), and after the install the
operator PUTs the whole feed on the door (PUT /feed, mutual TLS, the
operator's certificate only). A feed monetize-server would not start on (for
example a priced product with no vendor) is refused whole, by name.
The gRPC services
gRPC is the only control plane. The one exception is the vendor webhook, which is HTTP because the vendor speaks HTTP.
| service | what it answers |
|---|---|
Products | which product plugins are linked, the meters each declares and whether a cap on one is policed, the deployment's price lists and its one policy, each product's reachability, and the spend against each cloud's ceiling |
Purchase | a quote for an order, the start of the reserve → provision → capture transaction, the state of an attempt, and the abandonment of one |
Usage | one tenant's meters, every tenant's at once, a meter's history, its trend and how long there is, and the fleet total against what the iron can serve |
Ledger | the append-only rows for a reference or a tenant, what is still open, a deterministic text export of the operational state, and attempts folded to one line each |
Entitlement | what monetize believes about a tenant, signed: read one, list them, set one, a whole signed snapshot, the public key a product verifies with, reclaim of a lapsed grant, and what a tenant currently holds |
Signup | the state of one self-service signup, polled by the return page a payment vendor sends the customer back to |
SignupAdmin | every signup, including the refused ones — the operator's read, deliberately a second service so the return page cannot enumerate them |
Events | one bounded stream of what happened, so the console stops polling; every stream opens with an epoch and a gap is reported rather than hidden |
Thanks
First to its inventors, Vetra Automation AB and Ignalina ApS (Danskjävlarna).
monetize uses excellent software from others: the neat redb (Christopher Berner).
Sponsors
None so far — we would love to have one. Contact gunnar@gunnar.rs.