Billing¶
HomeCloud Billing is a first-class console service (/console/billing). Overview has a calendar Glance (Spending + Forecast) and a Billing Explorer below for range analysis — Cost Explorer–style chart and breakdown with HomeCloud design, not an AWS UI clone.
The meter stores quantities only. Billing does Usage × net USD catalog price = Charge. VAT is a separate invoice line — never baked into SKU prices. Homelab still issues invoices. Card payment is not enabled — Mark paid is manual only.
Compute list prices come from the Compute catalog: provider wholesale → operator FX (EURUSD) → GTM markup in the 2×–4× band (default 3×). Machine hours use the fulfilled Offering/SKU (offering_id), snapshotted in USD. Other services (SO, MQ, Mail, …) may still use temporary placeholder rates until they have wholesale.
Compute meters¶
| Metric | What is billed |
|---|---|
compute.machine.hours |
RUNNING machine × time, at the offering-derived snapshot |
compute.volume.gb_hours |
Volume GiB × time |
compute.snapshot.gb_hours |
Snapshot GiB × time |
compute.lb.hours |
Active load balancer × time |
compute.vpc.hours |
Active VPC × time (often $0 if the vendor network is free) |
compute.fip.hours |
Allocated Floating IP × time |
compute.egress.gb |
Observed Compute egress GiB |
Sticky public IPv4 on a NIC is not a reserved-IP SKU. Overlay gateways are not customer machines and are not billed. IPv6 address SKUs are not in this catalog.
One formula; Explorer can expand. Every Compute meter uses FX(wholesale) × GTM markup (2×–4×, default 3×). Invoice lines stay folded by metric. In the console, expand a Compute service row to see per-resource quantities (from a compact daily rollup — not a scan of every meter tick). A Currently billable strip shows what holdings are accruing right now, so historical machine hours in the range are not confused with live VMs.
Deleting a machine marks attached boot/data volumes as deleting so they stop metering immediately; purge then removes them. Detached volumes you keep remain billable until you delete them. Past machine hours stay on the chart with sticky/offering pricing.
Quantity is always wall-clock hours or GiB×time — never “effective hours” inflated by cost.
How usage is recorded¶
Meter is the only ledger. It stores quantities, not prices. The user operation never waits on Meter.
usage.refresh is a dirty hint only — it never creates a usage event. A worker claims the watermark (lock → read last_size/last_at → calculate → write usage + watermark in one transaction), so duplicate refreshes do not duplicate charges. The hint is published on the internal subject hc.usage.refresh (queue group), not on hc.events.> (browser SSE).
If the hint is dropped, the next hourly checkpoint or signed reconcile still reconstructs from durable state (fail-open).
Business fact (commit) → return immediately
↓
mark dirty + usage.refresh (identity only — not a bill)
↓
usage worker (lock watermark → delta → usage_events + usage_daily + usage_resource_daily + new watermark)
↓
Billing Explorer / invoice ← usage_daily × catalog/snapshot
Explorer expand ← usage_resource_daily × resolve
Currently billable ← Compute holdings
Raw usage_events remain for audit and reconcile. Opening Billing does not scan raw rows.
Three primitives only:
| Type | Meaning | When a row is written |
|---|---|---|
| A Occupancy | from → to |
stop/delete or hourly checkpoint |
| B Size × time | size × Δt |
size change or hourly checkpoint |
| C Counter | seq/id delta | after work, or drain (Mail / Functions) |
| Service | Model |
|---|---|
| Compute machine, LB, FIP, VPC | A |
| Compute volume / snapshot | B |
| Compute egress | C |
SO, IR, MQ backlog (mq.gb_hours) |
B |
| MQ publish / deliver | C |
| Mail, Functions | C / drain |
| Secrets | A at account (quota/table, not a 60s cluster list) |
| MDB | A (instance hours) + B (storage) |
| Redis | A |
mq.message_hours is not billed in v1. There is no per-minute holdings writer.
# quantities only — no prices
homecloud usage list
homecloud usage list --group-by service_id --output json
PowerShell: the same commands (no quoting difference).
| Item | Value |
|---|---|
| Console | Billing → /console/billing |
| Catalog id | billing (global — ignores the region switcher) |
| Currency | USD |
| Payments (v1) | Manual (platform admin marks paid). Stripe is the next adapter, not this release. |
Billing Overview¶
Single page — Overview and Invoices tabs only (no separate Cost Explorer / Budgets routes).
Glance (calendar truth)¶
Fixed at the top of Overview. Never follows the Explorer date range.
| Card | Behavior |
|---|---|
| Spending | Current calendar month MTD and previous calendar month Final in one card. MoM compares MTD to the same elapsed period last month. |
| Forecast | Projected full cost of the current calendar month (existing forecast API). MoM vs previous month total. |
There is no Overview Estimate KPI and no Overview VAT KPI. VAT stays on invoices.
Billing Explorer (user analysis)¶
One card below Glance: date range + Day / Week / Month, then the cost chart. Controls affect only the chart and the breakdown card under it.
| Area | Behavior |
|---|---|
| Date range | Range calendar (presets + custom). Days billed as UTC. |
| Granularity | Day / Week / Month (week buckets are Monday-start UTC weeks from daily series) |
| Cost over time | Same Explorer card as the filters. Stacked bars grouped by service. Each period has a fixed slot (inner chart scrolls sideways). Monthly canvas is at least 6 UTC months through the current month ($0 padding). |
| Cost breakdown | One row per service; expand for SKU type, then totals per resource kind |
| Invoices | Generate on demand; Mark paid is manual; VAT appears on invoice totals |
| Spend alerts | Notify only — never stop or suspend resources |
Object Storage cost accumulates while objects exist (GB × time). A large SO figure after “recent” Monitoring activity usually means existing objects were metered across the selected days — not only new uploads.
Reading a Compute expand¶
Expanding Compute shows three levels, all views of the same amount — nothing is billed twice:
- SKU rows — machine hours, volume GB·h, egress GB
- Resource-kind rows — machines / volumes / snapshots / load balancers …, each with how many resources contributed in the range
- Top resources (optional) — the biggest individual resources, marked billing now or historical; remaining resources stay folded into the kind totals
Volume quantity is capacity × time, so the helper line reads as average total capacity across the range (all volumes summed), not the size of one disk. Machine quantity is wall-clock hours.
Days are the days it happened¶
Holdings usage is filed on the UTC day it occurred, including when a machine stops or a volume disappears from inventory: a multi-day backlog is split into one entry per day. If the cost chart ever shows a spike on today for resources that stopped earlier, the rollups predate this behavior — rebuild them:
# dry run
python scripts/rebuild_usage_rollups.py --from 2026-09-01 --to 2026-09-14
# write
python scripts/rebuild_usage_rollups.py --account <account-uuid> --from 2026-09-01 --to 2026-09-14 --apply
The script re-spreads usage_events (whose source_id carries the interval start) across the days each interval covers and rewrites usage_daily / usage_resource_daily. Amounts do not change — only which day they land on.
Timezone contract (v1)¶
Daily buckets are UTC calendar days. The explore API returns "timezone": "UTC". The UI labels this explicitly. Per-account local timezone is not implemented yet.
API¶
Returns timezone, estimate, daily_series, by_service, prices_are_placeholder, has_usage, and has_unpriced_usage.
Returns groups (one aggregate per resource kind), items (top limit resources), resource_count, and truncated.
GET …/billing/forecast stays separate for the current month.
Spend alerts vs hard caps¶
Spend alerts notify only. Crossing a threshold never stops Compute or suspends resources. There is no hard spend limit in v1. Resource quotas (machine count, etc.) remain a separate Identity/quotas control.
A background check runs about every 15 minutes (billing_alert_interval_seconds). When the estimate is at or above the threshold:
- Owners and admins get one in-app notification (bell + Notifications, source Billing) linking to Billing → Alerts.
- The same people get one email (system mail). A failed send is not retried for that period.
- SSE
spend.alertrefreshes the bell. The same fire does not write a second event type.
Do not send twice in the same period. Each alert stores last_fired_period. A month window fires at most once per UTC calendar month (YYYY-MM). A week window compares the last 7 days and fires at most once per ISO week (YYYY-Www). Two different alerts (week + month) can each fire independently.
| Window | Spend compared | Dedup period |
|---|---|---|
| Calendar month (UTC) | Month-to-date estimate | Once that month |
| Last 7 days | Rolling 7-day estimate | Once that ISO week |
CLI¶
homecloud usage list
homecloud billing summary
homecloud billing invoices
homecloud billing forecast --horizon 7
PowerShell: the same commands (no quoting difference).
Permissions¶
billing.read— viewer+billing.write— owner/admin (generate invoice, mark paid, manage alerts)