Add self-serve billing: tiers, credits, BTCPay and Zaprite
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configFile } from '../file-models/config.json'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
// Connects the relay to the operator's Zaprite account so users can buy a
|
||||
// prepaid Pro/Max period with a CARD (the "Pay by card" rail alongside the
|
||||
// Bitcoin/BTCPay rail). Leave the API key blank to disable the card rail —
|
||||
// the app hides "Pay by card" and the Bitcoin rail keeps working.
|
||||
//
|
||||
// To get your Zaprite API key:
|
||||
// 1. Zaprite → Settings → API → create a key
|
||||
// 2. Paste it below (stored masked)
|
||||
//
|
||||
// To set up the webhook in Zaprite:
|
||||
// 1. Zaprite → Settings → Webhooks → add endpoint
|
||||
// 2. URL: https://<your-relay-host>/relay/zaprite/webhook
|
||||
// 3. Subscribe to order paid / completed events
|
||||
// (No webhook secret is needed — the relay re-fetches each order from
|
||||
// Zaprite's authenticated API to confirm payment before granting.)
|
||||
//
|
||||
// Card prices are charged in the currency below (cents for USD). They're
|
||||
// separate from the dashboard's "Set Tier Prices (USD)" accounting figure;
|
||||
// these are the real amounts a card buyer pays. Default ≈ parity with the
|
||||
// sat prices ($21 / $42) — raise them to add a premium for card fees.
|
||||
const inputSpec = InputSpec.of({
|
||||
relay_zaprite_api_key: Value.text({
|
||||
name: 'Zaprite API Key',
|
||||
description:
|
||||
'API key from Zaprite → Settings → API. Used to create hosted card checkouts and to re-fetch orders for webhook verification. Leave blank to disable the card rail.',
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
minLength: 0,
|
||||
maxLength: 256,
|
||||
}),
|
||||
relay_zaprite_base_url: Value.text({
|
||||
name: 'Zaprite Base URL',
|
||||
description:
|
||||
'Zaprite API base URL. Leave as the default unless Zaprite tells you otherwise.',
|
||||
required: false,
|
||||
default: 'https://api.zaprite.com',
|
||||
minLength: 0,
|
||||
maxLength: 256,
|
||||
patterns: [
|
||||
{
|
||||
regex: '^(https?://.+)?$',
|
||||
description: 'Must be empty or start with http:// or https://',
|
||||
},
|
||||
],
|
||||
}),
|
||||
relay_zaprite_currency: Value.text({
|
||||
name: 'Card Currency',
|
||||
description:
|
||||
'Fiat currency the card is charged in (ISO code, e.g. USD, EUR). The card prices below are in this currency.',
|
||||
required: false,
|
||||
default: 'USD',
|
||||
minLength: 0,
|
||||
maxLength: 8,
|
||||
}),
|
||||
pro_card_price: Value.number({
|
||||
name: 'Pro — Card Price',
|
||||
description:
|
||||
'Amount a card buyer pays for one prepaid Pro period, in the card currency. Default ≈ parity with the 21,000-sat Bitcoin price.',
|
||||
required: true,
|
||||
default: 21,
|
||||
min: 0,
|
||||
max: 100_000,
|
||||
integer: false,
|
||||
step: 0.01,
|
||||
placeholder: null,
|
||||
}),
|
||||
max_card_price: Value.number({
|
||||
name: 'Max — Card Price',
|
||||
description:
|
||||
'Amount a card buyer pays for one prepaid Max period, in the card currency. Default ≈ parity with the 42,000-sat Bitcoin price.',
|
||||
required: true,
|
||||
default: 42,
|
||||
min: 0,
|
||||
max: 100_000,
|
||||
integer: false,
|
||||
step: 0.01,
|
||||
placeholder: null,
|
||||
}),
|
||||
})
|
||||
|
||||
export const setZapriteConnection = sdk.Action.withInput(
|
||||
'set-zaprite-connection',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Set Zaprite Connection (card purchases)',
|
||||
description:
|
||||
'Wire the relay to your Zaprite account so users can buy Pro/Max with a card. Leave the API key blank to disable the card rail — the Bitcoin rail keeps working without it. Remember to add the webhook in Zaprite pointing at https://<your-relay-host>/relay/zaprite/webhook',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: 'Tiers',
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
async ({ effects }) => {
|
||||
const config = await configFile.read().once()
|
||||
let cents: any = {}
|
||||
try {
|
||||
cents = JSON.parse(config?.relay_tier_prices_fiat_cents_json || '{}')
|
||||
} catch {
|
||||
cents = {}
|
||||
}
|
||||
const toMajor = (c: any, fallback: number) =>
|
||||
typeof c === 'number' && Number.isFinite(c) ? c / 100 : fallback
|
||||
return {
|
||||
relay_zaprite_api_key: config?.relay_zaprite_api_key || undefined,
|
||||
relay_zaprite_base_url:
|
||||
config?.relay_zaprite_base_url || 'https://api.zaprite.com',
|
||||
relay_zaprite_currency: config?.relay_zaprite_currency || 'USD',
|
||||
pro_card_price: toMajor(cents?.pro, 21),
|
||||
max_card_price: toMajor(cents?.max, 42),
|
||||
}
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
// Card prices are entered in major units (dollars) but Zaprite + the
|
||||
// relay charge in the currency's smallest unit (cents), so ×100.
|
||||
const fiatCents = {
|
||||
pro: Math.round(Number(input.pro_card_price ?? 21) * 100),
|
||||
max: Math.round(Number(input.max_card_price ?? 42) * 100),
|
||||
}
|
||||
await configFile.merge(effects, {
|
||||
relay_zaprite_api_key: (input.relay_zaprite_api_key || '').trim(),
|
||||
relay_zaprite_base_url: (
|
||||
input.relay_zaprite_base_url || 'https://api.zaprite.com'
|
||||
).trim(),
|
||||
relay_zaprite_currency: (input.relay_zaprite_currency || 'USD')
|
||||
.trim()
|
||||
.toUpperCase(),
|
||||
relay_tier_prices_fiat_cents_json: JSON.stringify(fiatCents),
|
||||
})
|
||||
return null
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user