Platform-paid
Some operations spend the app’s own authority: the validator quorum threshold-signs them and the platform’s operator/treasury account pays the chain fee.
A BaaS is only honest if you can see what you’re paying for. Every billable operation your app performs — a function invocation, a stored object, a published message — emits a consumption event. Those events are aggregated per app, priced against your tier, and reconciled with your subscription. No black-box billing.
Each BaaS service reports usage in one small, uniform shape: the appId, a
category (database, functions, messaging or storage), the
operation (for example fn_deploy, fn_invoke or msg_publish), the
metered cost, and a timestamp. Reporting is fire-and-forget:
tracking failures never break the underlying operation. API calls are also
metered per category at the HTTP boundary, so coverage does not depend on every
service reporting by hand.
Your subscription tier defines what’s included and how overage is priced;
the price calculator turns aggregated consumption into an amount owed. Because
the events are categorized (database, functions, messaging, storage),
bills are itemized by surface rather than a single opaque number.
Functions and storage also enforce resource limits tied to your plan, so you hit a clear limit instead of a surprise bill:
Function count — the host refuses to deploy past your app’s function limit.
Function invocations are tier-aware quotas. A customer-attributed invocation is capped per end-user per day, derived from your tier’s monthly function-invocation allowance divided by 30; an agent-triggered invocation is capped by a separate per-agent daily call cap and a per-agent daily USD spend cap priced against the served app’s tier — so one runaway AI agent can’t drain your subscription float. Over-limit returns HTTP 429; the reads are fail-open, so a metering blip never blocks a working function.
Storage capacity is tier-aware. The per-app capacity quota derives from your subscription tier instead of a flat default — a paying tier raises the ceiling; unlimited tiers are uncapped. This too is fail-open: a tier-lookup blip falls back to the flat default rather than blocking uploads. The bytes are counted in one atomic per-app counter that an upload reserves before the object leaves the host, so two uploads in flight together cannot both pass a check that only one fits; a failed upload gives its bytes back. An upload past the quota answers 507 (Insufficient Storage). The private object store also caps what one cluster’s apps can hold in total: an upload beyond that answers 507 as well.
Storage retrieval is priced and pre-authorized. Reading an object back out of storage is a metered operation, not a free side effect of having paid to store it.
A download that reads the private object store prices the
transferred bytes against the per-GB retrieval rate (ipfs.retrieval_gb,
distinct from the ipfs.ingest_gb rate charged on the way in; the price
lines keep their names) and checks your balance before the first byte is
served. Every authorized download is charged, including a host cache hit.
Cache hits price the authoritative stored size without reading the bytes
again. The host generates a fresh request ID per download; a retried internal
request reuses that ID and deduplicates only that charge. Public callers
cannot choose this billing identity. Missing or unavailable billing refuses
paid reads.
Retrieval also enforces ownership: you can read back only content your own app stored — a request for an id you don’t own answers 404, indistinguishable from one that was never stored, so the endpoint can’t be probed for what a cluster holds. Unlike a write — which has already stored your bytes by the time it bills, and so fails soft — a read whose charge can’t be recorded is refused (HTTP 503) rather than served for free; the read mutated nothing, so a retry is safe.
Metered calls on the host and the gateway are charged for real by default: the cost is deducted from your app’s prepaid float, not just recorded. A charge is only deducted once your accrued cost actually crosses a whole billable unit (see Fractional metering below).
This is the path each metered host or gateway call takes:
functions, database, messaging, storage, dns, lb,
agent execution, …). This is an exact decimal figure, e.g.
0.0120105693….An operator can turn real charging off for a deployment; costs are then still computed and recorded, but nothing is deducted. The validator’s own per-operation fee on transaction prepares is built, not yet enabled.
A batch call is priced by the work it does, not as one call. The cost of a
route carries its declared cost weight, so a request that does N units of
billable work costs N — not 1. The DNS resolver’s batch endpoint
(POST /dns/resolve/batch) is the worked example: a 10-query batch is priced as
ten dns queries, not a single lookup.
The charge path never breaks your operation. A rejected charge (e.g. an
insufficient-balance 402) is recorded, not thrown back at you — your call
still returns.
The billable unit — one smallest-unit of HSUITE — is coarse. On XRPL, the
canonical HSUITE house, HST is a whole-unit IOU (0 decimals), so the smallest
unit is one whole HSUITE. Most metered operations are priced far below that:
a single DNS read might cost 0.0120105693… HSUITE.
Charging can only ever move a whole number of smallest-units (the validator’s
charge path requires a positive integer and rejects anything else). The
question is what to do with the sub-unit fraction on each call. Rounding each
operation up would charge a full 1 HSUITE for an op priced at 0.012
HSUITE — on cheap, high-volume operations, many times the published price.
So each op’s exact decimal cost is accrued into a per-app running remainder, and a charge is emitted only when the remainder crosses a whole smallest-unit — taking the crossed whole units by floor, never inventing one by ceil, and carrying the leftover fraction forward:
call 1: cost 0.6 remainder 0.0 → 0.6 charge nothingcall 2: cost 0.6 remainder 0.6 → 1.2 charge 1, carry 0.2call 3: cost 0.5 remainder 0.2 → 0.7 charge nothing…The guarantee is exact — computed in arbitrary-precision integer arithmetic, no float drift — over any sequence of operations:
Σ(charged) + finalRemainder == Σ(cost)You are never charged more than the exact published price of what you used, and never less by as much as a whole unit. The same accrual rule backs both the host and the gateway, so it applies wherever a metered op is charged. The running remainder is stored durably and shared by every replica that serves your app, so a restart does not lose or double-count the pending fraction.
Metering answers “how much,” but a subscription answers “are you allowed at all.” The most cost-bearing actions are gated on an active subscription before they run. Deploying or mutating a smart-app, for instance, first checks the caller’s wallet against the validator’s subscription record:
Because everything is scoped by appId, that identifier is itself a tenant
boundary. The routes that change a subscription fail closed on a malformed
appId: a genuinely absent value still defers to the route’s own validation,
but a present-but-invalid one is rejected with HTTP 400 before any lookup
runs. The same rule applies to the create path’s developerAccountId.
The canonical subscription truth lives on the validator; the host delegates to it. See the subscriptions section for tiers, renewal, and how subscription NFTs work.
Not every write is billed to the same party, and the host treats the two kinds very differently.
Platform-paid
Some operations spend the app’s own authority: the validator quorum threshold-signs them and the platform’s operator/treasury account pays the chain fee.
Customer-funded
Others are customer-funded: the acting wallet pays and the platform spends nothing.
The app is billed in HSUITE for every successful platform-paid write. Because the platform fronts the on-chain fee on these routes, each successful one also levies a metered HSUITE charge against your app — on the same charge path and exactly-once ledger as every other metered call, priced from the pricing catalogs. This is live while real charging is on (the default, see Real charging is on by default) and is fail-soft like all host charging — a rejected charge never fails the write. The charge lands only on success: a refused, failed, or post-debit-ambiguous write charges nothing, and it is keyed per operation so one write is never billed twice.
| Platform-paid route | Charged as |
|---|---|
createToken (Hedera) |
token.create.hedera |
createToken (XRPL) |
token.create |
createAccount |
account.create.<chain> (chains without a price line: uncharged) |
createTopic |
topic.create |
rules/publish, prepare-create (non-agent) |
topic.submit_message per 1 KiB chunk |
createAgent, agents/register, agent prepare-create |
agent.register |
This HSUITE bill is separate from the per-app and per-customer spend caps below — the caps bound how many platform-paid writes you can make; this charge is what each successful one costs you.
Platform-paid writes require an app-owner principal. Publishing a rule, the single-shot entity creates (token / account / topic), and agent creation all make the quorum sign and the platform pay. A self-serve customer principal — any wallet that signed a session challenge naming your app — is refused with HTTP 403 on these routes; only your app’s own app-owner session may spend the app’s authority. (A customer session names an app to prove intent, never authority, which is why comparing app ids can’t gate this — the caller chose the id.)
A customer’s write your app relays needs your explicit sponsorship. When your
app-owner session forwards a customer (X-Customer-Session-Token or
X-Customer-Session-Claim — what forCustomer(token) and SCOPED_BAAS_CLIENT
send), the platform would be paying for that customer’s write, so these routes
answer HTTP 403 unless the request also says X-Platform-Paid-Sponsor: app
(SDK: { sponsorCustomerWrite: true } on the call). Vouching for who the customer
is does not mean you pay for them. Inside a SmartEngineModule backend the SDK
goes further: a platform-paid call made while a request is served to a customer
(not an @AppOwnerOnly() route) is refused before it is sent unless it is
sponsored, and a sponsored one carries that customer — even when it is made on a
BaasClient your app built itself. A sponsored relay counts against
your per-app cap and a per-customer rolling 24-hour ceiling keyed to the
customer’s validated wallet (not the session, so minting fresh sessions doesn’t
reset it): by default 10 writes per customer, set per deployment, where 0
refuses every relayed write. Over it returns HTTP 429.
A per-app spend cap bounds the platform’s exposure. Platform-paid writes are
metered against a per-app rolling 24-hour ceiling (by default 100 writes, set
per deployment; 0 hard-disables), counted from a self-refilling ledger keyed to
the session-bound appId (never a caller-supplied body field, header, or route
param, so it can’t be cheated). Over the ceiling returns HTTP 429 with an
honest Retry-After computed from the oldest in-window charge. The cap is
reserved before the validator hop, so a request refused before that hop —
over the ceiling, or rejected by the host first, like a rule that fails its
schema or an unknown rule reference — costs nothing and keeps no slot. Once the
validator has the request, its slot stays taken even if the request then fails:
the chain may already have charged the platform, so that outcome counts like a
completed write, for the full 24-hour window from the moment the validator was
handed it, and is never given back early.
Agent creation is capped twice. Each agent an app creates provisions a funded
multisig wallet the caller doesn’t pay for, so agent creation is likewise
app-owner-only and is bounded by a per-app ceiling (limits.agents, default
100) and a per-customer ceiling (limits.agentsPerCustomer, default 25), both
enforced before the DKG and before any funding transaction, and both per-app
limits (an explicit 0 hard-disables). When the app forwards a customer
session, that customer becomes the agent’s owner while the app wallet stays
the payer.
When an allowance slot comes back. An agent creation takes its slot when it is
admitted. One refused before the validator is handed it gives the slot back within
about an hour, and so does a two-phase draft that is never handed out. A draft that
is handed out but never executed expires after 24 hours and gives its slot back,
unless its address holds funds: that draft stays executable, and keeps its slot.
An execution whose funding payment the ledger refuses leaves the draft executable
too, and it expires like one never executed. Once the validator has an
agents.register or entities.createAgent request, the slot stays taken even if
the request then fails: the funded wallet may already exist.
A customer spends your agent allowance only when you sponsor them. Every route
that creates an agent is platform-paid: agents.register, entities.createAgent,
and the agent draft of the two-phase flow (prepare-create with
entityType: 'agent', XRPL only, which takes its allowance slot at prepare time and
keeps it once the draft is handed out, until it is executed; a draft that is never
handed out gives it back). A customer
your app relays there is refused (HTTP 403) unless the call sponsors them, and
a sponsored one is held to the per-customer ceilings as well as yours. So a free,
self-minted customer session cannot spend your app’s agent allowance at all
unless you chose to pay for it.
Customer-funded writes stay open to any bound session. The two-phase
prepare-create / execute-create entity flow (except an agent draft’s
prepare-create, above) and entity withdraws are funded by the acting session
wallet, so there’s no platform outflow to bound.
| Write | Who pays | Who may call |
|---|---|---|
| Rule publish · single-shot entity create · agent create (every route) | platform operator / treasury | app-owner session only, subject to the per-app cap(s); a relayed customer only when sponsored |
Two-phase prepare-create (except an agent draft) / execute-create · withdraw |
the acting session wallet | any bound session |
Read today’s remaining budget for an app with a single read-only call:
GET /api/v3/resources/remaining/:appIdIt resolves the app’s subscription tier and its subscription-NFT serial, subtracts today’s persisted usage from the per-day caps, and returns:
| Field | Meaning |
|---|---|
tier · nftSerial |
Resolved subscription tier and its NFT serial |
apiCallsPerDay |
The tier’s monthly validator-signing allowance divided by 30; null when the tier is unlimited |
bytesPerDay |
Always null: there is no daily byte cap |
apiCallsRemaining · bytesRemaining |
What’s left today — null = unlimited, 0 = exhausted |
apiCallsUsedToday · bytesUsedToday |
Usage so far today |
resetUtc |
Next UTC midnight, when the daily counters roll over |
Because usage is read from the persisted store, the numbers survive restarts. Related read-only routes: /api/v3/resources/consumption/:appId (plus /history, /ledger, /breakdown, /status) — see the API reference.