Разработка системы биллинга Node-as-a-Service
Запустить ноду несложно. Сложно правильно считать деньги — особенно когда unit of consumption это не запрос, а время работы ноды, тип сети, версия клиента и тариф, который выбрал пользователь три недели назад. NaaS биллинг провален в большинстве молодых провайдеров не потому что задача технически трудная, а потому что между "считать деньги примерно верно" и "считать деньги точно и прозрачно" — пропасть в несколько месяцев инженерной работы.
Модели тарификации: что реально используется
Pay-per-request — классика для RPC провайдеров (Alchemy, Infura, QuickNode). Считаем количество JSON-RPC вызовов, с весовыми коэффициентами по методам:
| Метод | Compute Units |
|---|---|
eth_blockNumber |
10 |
eth_getBalance |
19 |
eth_call |
26 |
eth_getLogs |
75 |
trace_transaction |
150 |
debug_traceTransaction |
500 |
eth_getLogs с широким диапазоном блоков — это атака на ноду. Без весовых коэффициентов пользователь может сделать один запрос стоимостью тысячи "обычных". Алхимия называет это Compute Units, QuickNode — Credits. Названия разные, смысл один.
Time-based (subscription) — выделенная нода фиксированной мощности оплачивается помесячно. Понятнее для пользователя, предсказуемый revenue для провайдера. Минус: пользователь переплачивает при низкой нагрузке.
Hybrid — базовый план с месячным включённым объёмом, overage billing сверху. Используют большинство зрелых провайдеров.
Архитектура биллинговой системы
Слой сбора метрик
Критический путь: каждый RPC запрос должен быть залогирован до ответа пользователю — иначе при краше теряем данные об использовании. Архитектура:
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 пишет в Apache Kafka или NATS JetStream — оба дают at-least-once delivery. Синхронная запись в базу на каждый запрос убивает latency (мы добавляем 100–500мс к каждому RPC вызову, что недопустимо).
// Async metric emission — не блокирует запрос
func (b *BillingMiddleware) RecordUsage(ctx context.Context, event UsageEvent) {
select {
case b.eventChan <- event:
// успешно поставлено в буфер
default:
// буфер полон — метрика потеряна, логируем как sampling loss
b.metrics.IncSamplingLoss()
}
}
Допустимый процент sampling loss для биллинга: <0.01%. Если теряем больше — backpressure или нода умирает под нагрузкой.
Агрегация и rating engine
Raw события из Kafka → rating pipeline → billable records в PostgreSQL.
Rating — это применение тарифных правил к raw usage. Для 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
Агрегация происходит по временным окнам (5-минутные buckets), финальная запись — в конце расчётного периода. Это создаёт billing lag — пользователь потратил деньги, но видит обновление баланса через 5 минут. Это норма для NaaS.
Хранение данных
Для биллинга PostgreSQL — правильный выбор. Не ClickHouse, не MongoDB. Биллинг требует ACID при списании средств. Схема:
-- 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 партиционируется по дате — иначе через год таблица перестанет вмещаться в индексы оперативной памяти. Retention policy: raw events хранятся 90 дней, агрегаты — бессрочно.
Crypto-native биллинг: специфика
Prepaid баланс в стейблкоинах
Большинство NaaS для Web3 работают в prepaid модели: пользователь пополняет баланс в USDC/USDT, списание происходит с него. Это проще чем подписка через кредитную карту и нет chargeback рисков.
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);
}
}
Важный паттерн: billingOracle — не EOA, а multisig или сервис с HSM. Ключ от оракула компрометируется → все балансы под угрозой.
Автоматическое пополнение баланса
Low balance triggers — пользователь настраивает автопополнение при достижении порога:
interface AutoRefillConfig {
threshold: bigint; // пополнять когда баланс < threshold
refillAmount: bigint; // на сколько пополнять
sourceWallet: string; // кошелёк для автосписания (требует approve)
maxMonthlySpend: bigint; // защита от billing runaway
}
maxMonthlySpend — обязательная защита. Без неё баглый клиент делает миллион запросов и съедает весь баланс пользователя за час.
Алерты и rate limiting
Rate limiting на уровне API Gateway (не биллинга): 1000 req/sec per API key — стандартный дефолт. Без rate limiting один пользователь с багом может положить ноду для всех.
Billing alerts — нотификации при:
- Баланс упал ниже X% от обычного месячного расхода
- Резкий spike usage (>3x среднего за последний час)
- Нода недоступна (клиент платит за downtime — это должно компенсироваться SLA кредитами)
SLA credits — автоматическое начисление кредитов при downtime. Считается через uptime probe (внешний сервис мониторинга, не ваш собственный). Self-reported uptime 99.99% не вызывает доверия у корпоративных клиентов.
Что ломается в production
За время работы с NaaS биллингом встречали несколько нетривиальных проблем:
Clock skew между нодами — если billing proxy и нода имеют расхождение часов >1 сек, timestamps в usage events некорректны. NTP обязателен, предпочтительно chrony с Google NTP серверами.
Duplicate events при retry — Kafka at-least-once delivery означает дубликаты при retry. Каждое событие должно иметь idempotency key (request_id + node_id), rating engine де-дуплицирует перед записью.
Timezone bugs в billing cycles — расчётный период "1-е число месяца" в UTC. Пользователь из UTC-8 видит что цикл закрывается в 16:00 его времени. Нужна явная документация и по желанию — кастомные billing cycles.
Сроки разработки полноценной NaaS billing системы: 3–5 месяцев для команды из 2–3 backend инженеров. MVP с prepaid балансом и базовым rate limiting — 6–8 недель.







