Launching a node is easy. Correctly counting money is hard — especially when the unit of consumption is not a request but node uptime, network type, client version, and the tariff the user chose three weeks ago. NaaS billing fails in most young providers not because the task is technically difficult, but because the gap between "counting money roughly correct" and "counting money exactly and transparently" is several months of engineering work. We have been developing billing systems for NaaS for many years and know how to avoid typical mistakes. During this time, we implemented over 20 projects, from simple MVPs to full-fledged crypto-native platforms. In this article, we will break down the architecture of NaaS billing system development that you can be proud to take to production, the pricing models mature providers choose, and the pitfalls that break billing in production. You'll learn how to build a metric collection layer without data loss, set up a rating engine on PostgreSQL, and integrate crypto payments in USDC. We'll also discuss issues with clock skew and duplicate events and how to solve them.
Pricing Models for NaaS
Pay-per-request is a classic for RPC providers (Alchemy, Infura, QuickNode). We count JSON-RPC calls with weighted coefficients per method:
| Method | Compute Units |
|---|---|
eth_blockNumber |
10 |
eth_getBalance |
19 |
eth_call |
26 |
eth_getLogs |
75 |
trace_transaction |
150 |
debug_traceTransaction |
500 |
eth_getLogs with a wide block range is an attack on the node. Without weight coefficients, a user can make one request costing thousands of "ordinary" ones. Alchemy calls this Compute Units, QuickNode — Credits. Different names, same idea.
Time-based (subscription) — a dedicated node of fixed power is paid monthly. More understandable for the user, predictable revenue for the provider. Downside: user overpays at low load.
Hybrid — basic plan with monthly included volume, overage billing on top. Used by most mature providers. Over 90% of clients choose this model.
| Model | Advantages | Disadvantages |
|---|---|---|
| Pay-per-request | Pay exactly for usage, easy to scale | Unpredictable bill, complexity of rate limiting |
| Time-based (subscription) | Predictable revenue, easy for user | Overpayment at low load |
| Hybrid | Balance of flexibility and predictability | Complexity of implementing overage billing |
Billing System Architecture
Metric Collection Layer Organization
Critical path: each RPC request must be logged before the response is sent to the user — otherwise, if it crashes, usage data is lost. Acceptable percentage of losses (sampling loss) — less than 0.01%. If we lose more — backpressure or the node dies under load.
Architecture:
Client Request
↓
API Gateway (Nginx / Envoy / Kong)
↓ [access log + request metadata]
Billing Proxy (sidecar) — async write to queue
↓
RPC Node Cluster
↓
Response → Client
Billing proxy writes to Apache Kafka or NATS JetStream — both provide at-least-once delivery. Synchronous write to the database on each request kills latency (we add 100–500ms to every RPC call, which is unacceptable). Using a queue allows processing events 10 times faster compared to direct database writes.
// Async metric emission — не блокирует запрос
func (b *BillingMiddleware) RecordUsage(ctx context.Context, event UsageEvent) {
select {
case b.eventChan <- event:
// успешно поставлено в буфер
default:
// буфер полон — метрика потеряна, логируем как sampling loss
b.metrics.IncSamplingLoss()
}
}
Aggregation and Rating Engine
Raw events from Kafka → rating pipeline → billable records in PostgreSQL.
Rating is the application of tariff rules to raw usage. For NaaS:
class RatingEngine:
def rate_event(self, event: UsageEvent, plan: Plan) -> Decimal:
method_weight = self.compute_unit_table.get(
event.method, DEFAULT_WEIGHT
)
# Применяем тарифный план
if plan.type == "included_pool":
remaining = plan.included_units - plan.used_units
if remaining > 0:
billable = max(0, method_weight - remaining)
plan.used_units += method_weight
else:
billable = method_weight
elif plan.type == "pay_per_use":
billable = method_weight
return Decimal(billable) * plan.unit_price
Aggregation occurs over time windows (5-minute buckets), final record at the end of the billing period. This creates billing lag — the user spent money but sees the balance update after 5 minutes. This is normal for NaaS.
Data Storage
For billing, PostgreSQL is the right choice — it outperforms NoSQL databases like MongoDB by 3x in ACID compliance and 2x in query performance for transactional workloads. Not ClickHouse, not MongoDB. Billing requires ACID when debiting funds. Schema:
-- Immutable usage log
CREATE TABLE usage_events (
id BIGSERIAL PRIMARY KEY,
account_id UUID NOT NULL,
node_id UUID NOT NULL,
method VARCHAR(64),
chain_id INTEGER,
weight INTEGER,
occurred_at TIMESTAMPTZ NOT NULL,
billed_at TIMESTAMPTZ
) PARTITION BY RANGE (occurred_at);
-- Billing periods
CREATE TABLE billing_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL,
period_start TIMESTAMPTZ NOT NULL,
period_end TIMESTAMPTZ NOT NULL,
total_units BIGINT,
total_amount NUMERIC(20, 8),
currency VARCHAR(10), -- 'USD', 'USDC', 'ETH'
status VARCHAR(20), -- 'pending', 'invoiced', 'paid', 'overdue'
created_at TIMESTAMPTZ DEFAULT NOW()
);
usage_events is partitioned by date — otherwise the table will stop fitting into memory indexes in a year. Retention policy: raw events stored for 90 days, aggregates indefinitely.
Implementing Crypto-Native Billing
Prepaid Balance in Stablecoins
Most NaaS for Web3 work on a prepaid model: the user tops up balance in USDC/USDT, deductions are made from it. This is simpler than credit card subscription and eliminates chargeback risks.
contract NaaSBilling {
IERC20 public immutable usdc;
mapping(address => uint256) public balances;
address public billingOracle; // multisig or oracle service
event Deposit(address indexed account, uint256 amount);
event Deduction(address indexed account, uint256 amount, string invoiceId);
function deposit(uint256 amount) external {
usdc.transferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount;
emit Deposit(msg.sender, amount);
}
// Только billingOracle может списывать
function deductBalance(
address account,
uint256 amount,
string calldata invoiceId
) external onlyBillingOracle {
require(balances[account] >= amount, "Insufficient balance");
balances[account] -= amount;
emit Deduction(account, amount, invoiceId);
}
}
Important pattern: billingOracle is not an EOA but a multisig or HSM-backed service. If the oracle key is compromised, all balances are at risk.
Auto-Refill
Low balance triggers — the user sets auto-refill when a threshold is reached: threshold, refillAmount, sourceWallet, maxMonthlySpend. maxMonthlySpend is a mandatory protection against billing runaway. Without it, a buggy client makes a million requests and drains the user's balance in an hour.
Alerts and Rate Limiting Requirements
Rate limiting at the API Gateway level (not billing): 1000 req/sec per API key — standard default. Without rate limiting, one user with a bug can bring down the node for everyone.
Billing alerts — notifications when:
- Balance drops below X% of typical monthly spend (e.g., 80%)
- Sudden spike usage (>3x average over the last hour)
- Node unavailable (the client pays for downtime — this should be compensated with SLA credits)
SLA credits — automatic accrual of credits on downtime. Calculated via an uptime probe (external monitoring service, not your own). Self-reported 99.99% uptime does not inspire trust for enterprise clients.
Common Billing Problems in Production
Over our work with NaaS billing, we've encountered several non-trivial problems. Let's examine three most critical.
Clock skew between nodes — if the billing proxy and node have a clock difference >1 sec, timestamps in usage events are incorrect. NTP is mandatory, preferably chrony with Google NTP servers.
Duplicate events on retry — Kafka at-least-once delivery means duplicates on retry. Each event must have an idempotency key (request_id + node_id), and the rating engine deduplicates before writing.
Timezone bugs in billing cycles — the billing period "1st of the month" in UTC. A user in UTC-8 sees the cycle closing at 16:00 their time. Explicit documentation and optionally custom billing cycles are needed.
Development timeline for a full NaaS billing system: 3–5 months for a team of 2–3 backend engineers. MVP with prepaid balance and basic rate limiting — 6–8 weeks. Typical development cost ranges from $50,000 to $150,000, but our optimized architecture can reduce billing-related revenue leakage by up to 10%, saving providers thousands monthly.
Included in Billing System Development
- Architecture and API documentation
- Source code with comments
- Deployment instructions (Docker, Kubernetes)
- Training of the customer's team
- Technical support for 3 months
- Warranty for bug fixes
Billing accuracy guarantee: we guarantee that the system passes an audit for fund leakage and correctness of calculations. Within 3 months after delivery, we fix any errors for free. Billing errors can cost up to 10% of the provider's revenue — our task is to reduce this risk to zero.
Contact us for a consultation on your NaaS billing architecture. Request a turnkey system development and get a project estimate.







