Add self-serve billing: tiers, credits, BTCPay and Zaprite

This commit is contained in:
Keysat
2026-06-13 13:36:05 -05:00
parent 84d56c94c9
commit 0aa648706e
17 changed files with 3781 additions and 116 deletions
+93
View File
@@ -0,0 +1,93 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
const { InputSpec, Value } = sdk
// Operator-set per-tier monthly subscription prices, in USD. Used by
// the dashboard to compute revenue and operating margin (Gemini cost
// already comes out of the audit log). Pure accounting — the relay
// itself does no billing.
const inputSpec = InputSpec.of({
core_price: Value.number({
name: 'Core (Free) — Monthly Price',
description:
'Monthly subscription price for the Core tier in USD. Typically $0 since Core is the free entry tier. Used by the dashboard to compute total revenue; leave at 0 unless you actually charge for Core.',
required: true,
default: 0,
min: 0,
max: 10_000,
integer: false,
step: 0.01,
units: 'USD',
placeholder: null,
}),
pro_price: Value.number({
name: 'Pro — Monthly Price',
description:
'Monthly subscription price for the Pro tier in USD. Should match what you actually charge Pro customers on the licensing side.',
required: true,
default: 5,
min: 0,
max: 10_000,
integer: false,
step: 0.01,
units: 'USD',
placeholder: null,
}),
max_price: Value.number({
name: 'Max — Monthly Price',
description:
'Monthly subscription price for the Max tier in USD. Should match what you actually charge Max customers on the licensing side.',
required: true,
default: 15,
min: 0,
max: 10_000,
integer: false,
step: 0.01,
units: 'USD',
placeholder: null,
}),
})
export const setTierPrices = sdk.Action.withInput(
'set-tier-prices',
async ({ effects }) => ({
name: 'Set Tier Prices (USD)',
description:
'Configure the monthly USD price you charge per tier. The dashboard uses these numbers to compute revenue and operating margin against Gemini API cost. Has no effect on actual billing — it is for the operators accounting view only.',
warning: null,
allowedStatuses: 'any',
group: 'Tiers',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
let parsed: any = {}
try {
parsed = JSON.parse(config?.relay_tier_prices_usd_json || '{}')
} catch {
parsed = {}
}
return {
core_price: typeof parsed?.core === 'number' ? parsed.core : 0,
pro_price: typeof parsed?.pro === 'number' ? parsed.pro : 5,
max_price: typeof parsed?.max === 'number' ? parsed.max : 15,
}
},
async ({ effects, input }) => {
const prices = {
core: Number(input.core_price ?? 0),
pro: Number(input.pro_price ?? 5),
max: Number(input.max_price ?? 15),
}
await configFile.merge(effects, {
relay_tier_prices_usd_json: JSON.stringify(prices),
})
return null
},
)