Files
zeekayandHanzo Dev c32f0ba089 Bill compute on cloud's commerce API as visor's IAM application
Balance, spend-cap verdict and usage go to /v1/billing on cloud with
visor's client_credentials token and the org in X-Org-Id. The balance
read refuses an answer for another org; the usage id is the unit
(machine id at launch, machine and hour in a sweep), which cloud debits
once. commerce/metering and COMMERCE_SERVICE_TOKEN are gone.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 02:59:54 -07:00

209 lines
8.4 KiB
Go

// Copyright 2025 Hanzo Industries Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// fleet.go is the fleet-billing orchestrator — the ONE place the three connected-
// compute tiers are metered, all through the one commerce client (service.Record),
// per org+project, so a customer sees ONE invoice
// however their compute is connected:
//
// (a) BYOC cloud account → 1% of the account's cloud spend (daily, incremental).
// (b) BYO hardware/device → flat $1/mo per connected GPU/device (monthly).
// (c) hanzo.network validator → FREE (verified on-chain; never billed).
//
// The tier is a property of the compute source (a Provider vs a FleetWorker.Kind),
// resolved here; there is one debit path, three rates. Money safety: every unit is
// claimed cluster-wide via object BillingLease before it is billed, and debited
// under the unit's own id, which the ledger debits once.
// Honesty: no spend or exemption is ever fabricated — an unreadable cost is skipped
// (no fee) and an unverified validator is billed (flagged), never the reverse.
//
// Brand policy: the upstream cloud TYPE (AWS/DO/…) never leaves the cost readers; the
// metering line's Model carries only the tier and the customer's OWN provider/worker
// label, never an upstream provider name.
package billing
import (
"context"
"fmt"
"time"
"github.com/hanzoai/compute/chain"
"github.com/hanzoai/compute/logs"
"github.com/hanzoai/compute/object"
"github.com/hanzoai/compute/service"
"github.com/hanzoai/compute/telemetry"
)
// Fleet-billing tiers (the metering line's Status + telemetry tier).
const (
tierBYOC = "byoc"
tierDevice = "device"
)
// deviceMonthlyCents is the flat tier-(b) rate: $1.00 per connected device per month.
const deviceMonthlyCents = 100
// onePercentFeeCents is the tier-(a) rate: 1% of a cloud-spend increment, rounded to
// the nearest cent (half up). Zero/negative spend yields no fee.
func onePercentFeeCents(spendCents int64) int64 {
if spendCents <= 0 {
return 0
}
return (spendCents + 50) / 100
}
// meterFleetLine is the ONE fleet debit: it records cents to the org's commerce
// ledger, attributed to org+project, and mirrors it as an OTel metric. Service is
// the brand-neutral "fleet"; Model is "<tier>:<label>" where label is the
// customer's OWN provider/worker name (never an upstream cloud name). unit names
// the billing unit the caller already claimed, and is the debit's id.
func meterFleetLine(ctx context.Context, org, project, tier, label string, cents int64, unit string) {
if cents <= 0 {
return
}
if err := service.Record(ctx, service.Charge{
ID: unit, Org: org, Project: project, Cents: cents,
Service: "fleet", Model: tier + ":" + label,
}); err != nil {
logs.Warning("fleet billing: meter %s line for org %s (%d cents): %v", tier, org, cents, err)
return
}
telemetry.CountMetered(ctx, org, project, tier, cents)
logs.Info("fleet billing: metered %s %d cents to %s (project=%q, unit=%s)", tier, cents, org, project, unit)
}
// CollectBYOCCosts is the daily tier-(a) collector. For every org's active BYOC cloud
// provider it reads month-to-date spend via that cloud's cost API and meters 1% of the
// increment since the last run to org+project. No spend → no fee; an unreadable cost
// (unconfigured scope, transient error) is skipped for this run and recovered on the
// next (the cursor is only advanced on a successful read). Single-flight per
// (provider, day) via BillingLease, so replicas never double-bill.
func CollectBYOCCosts(ctx context.Context, now time.Time) {
if !service.Billable(ctx, "byoc.daily") {
return
}
ctx, span := telemetry.Span(ctx, "billing.collect.byoc", "", "")
defer span.End()
providers, err := object.GetAllActiveCloudProviders()
if err != nil {
span.RecordError(err)
logs.Warning("fleet billing: list BYOC providers: %v", err)
return
}
day := now.UTC().Format("20060102")
month := now.UTC().Format("200601")
billed := 0
for _, p := range providers {
reader, err := service.NewCostReader(p.Type, p.ClientId, p.ClientSecret, p.Region, p.CostReadScope)
if err != nil {
continue // ErrCostUnavailable etc. — cost-read not wired for this account; no fee
}
mtd, err := reader.MonthToDateCents(ctx, now)
if err != nil {
logs.Warning("fleet billing: read spend for %s/%s: %v", p.Owner, p.Name, err)
continue // transient/unavailable → skip this run, recover next (cursor untouched)
}
if mtd <= 0 {
continue // no spend → no fee
}
// Claim the day for this provider AFTER a successful read, so only the lease
// winner advances the cursor and bills; losers skip.
unit := fmt.Sprintf("byoc:%s:%s:%s", p.Owner, p.Name, day)
if !object.ClaimBillingUnit(unit, now) {
continue
}
delta, err := object.AdvanceCostCursor(p.Owner, p.Name, month, mtd)
if err != nil {
logs.Warning("fleet billing: advance cost cursor %s/%s: %v", p.Owner, p.Name, err)
continue
}
fee := onePercentFeeCents(delta)
if fee <= 0 {
continue
}
meterFleetLine(ctx, p.Owner, p.Project, tierBYOC, p.Name, fee, unit)
billed++
}
logs.Info("fleet billing: BYOC cost sweep done (providers=%d billed=%d)", len(providers), billed)
}
// meterWorkerDevice bills ONE connected worker for the current month: the flat
// $1/device tier-(b) rate — UNLESS the worker is a verified hanzo.network validator
// (tier c), which is free. The validator exemption is granted ONLY on a positive,
// verified on-chain read; an unwired lookup or a lookup error leaves the worker
// billed as a device, flagged in the log (never fabricate an on-chain exemption).
// Single-flight per (worker, month) via BillingLease, so connect-time and the monthly
// sweep can both call this without double-billing.
func meterWorkerDevice(ctx context.Context, w *object.FleetWorker, now time.Time) {
if w.Kind == object.FleetWorkerValidator {
exempt, status, err := chain.ValidatorExemption(ctx, w.ValidatorAddress)
if err != nil {
logs.Warning("fleet billing: validator lookup for %s/%s: %v", w.Owner, w.Name, err)
}
if exempt {
logs.Info("fleet billing: worker %s/%s is a verified validator — free (native)", w.Owner, w.Name)
return
}
logs.Info("fleet billing: worker %s/%s validator status=%q — billed as device", w.Owner, w.Name, status)
}
devices := max(w.DeviceCount,
// a connected box is at least one device
1)
cents := int64(devices) * deviceMonthlyCents
month := now.UTC().Format("200601")
unit := fmt.Sprintf("device:%s:%s:%s", w.Owner, w.Name, month)
if !object.ClaimBillingUnit(unit, now) {
return // already billed this worker this month (or another replica did)
}
meterFleetLine(ctx, w.Owner, w.Project, tierDevice, w.Name, cents, unit)
}
// MeterConnectedDevices is the monthly tier-(b)/(c) sweep: bill every CONNECTED BYO
// worker $1/device for the current month (validators exempt). It is safe to run more
// than once a month — the per-(worker, month) lease makes each worker's fee
// exactly-once.
func MeterConnectedDevices(ctx context.Context, now time.Time) {
if !service.Billable(ctx, "device.monthly") {
return
}
ctx, span := telemetry.Span(ctx, "billing.meter.devices", "", "")
defer span.End()
workers, err := object.GetConnectedFleetWorkers()
if err != nil {
span.RecordError(err)
logs.Warning("fleet billing: list connected workers: %v", err)
return
}
for _, w := range workers {
meterWorkerDevice(ctx, w, now)
}
logs.Info("fleet billing: device meter sweep done (connected=%d)", len(workers))
}
// BillWorkerOnConnect arms billing for a freshly-connected worker — the "armed on
// connect" half of tier (b). It bills the current month immediately (idempotent per
// month via the same lease the monthly sweep uses), so a box connected mid-month is
// billed at once, and the monthly sweep never double-bills it.
func BillWorkerOnConnect(ctx context.Context, w *object.FleetWorker) {
if !service.Billable(ctx, "device.connect") {
return
}
meterWorkerDevice(ctx, w, time.Now())
}