Concepts
- Plan — amount in USD-pegged USDC (6 decimals), interval (
day|week|month|year), optional trial, optional max cycles, accepted chains/assets. Price is immutable: create a new plan to change it. - Subscription — one customer on one plan. States:
incomplete → trialing → active → past_due → unpaid, pluspaused,canceled,expired. - Mandate — the customer’s on-chain authorization. Solana: a capped SPL delegate approval (never unlimited).
- Invoice — one per billing period. Gross, platform fee and net are stored in integer base units.
- Charge attempt — one signed send with a unique idempotency key. Ambiguous sends are reconciled on-chain before any retry.
- Delegate wallet — a dedicated Solana hot wallet that customers approve to pull USDC. It pays network fees and must hold SOL. It is never the vault or treasury key.
How Solana subscriptions work
- Merchant creates a plan.
- Merchant sends the hosted subscribe link (or creates a checkout through the API).
- Customer connects a Solana wallet and reviews the exact amount, interval, first charge and maximum authorization.
- Customer signs one
ApproveCheckedtransaction granting the Tribridge delegate a capped allowance:amount × min(max_cycles, default_cap_cycles). The default cap is 12 cycles. - Backend verifies the delegate, allowance, balance and token account.
- No trial: the first invoice is charged immediately. Trial: first charge at
trial_end. - On renewal: re-check the mandate, then the delegate signs one
TransferCheckedfor exactly one cycle to the treasury. - If the delegate was replaced or revoked, the allowance is too low, or the balance is insufficient, the backend does not send and starts dunning + a needs-reauthorization notice.
Create a plan
Create a hosted subscription checkout
{ subscription_id, checkout_url, subscription }. Send the customer to checkout_url.
API vs dashboard
API: create the plan once, then create one checkout per customer from your backend. Listen to signed webhooks. Best when subscriptions are embedded in your own product. Dashboard: create a plan, copy the hosted subscribe link, manage pause/resume/cancel, copy the customer portal link, view invoices and retry failed charges. Best for no-code merchants, testing and operations. Both use the same engine and tables.Lifecycle
- Customer reviews the terms and authorizes in their wallet.
- No trial: first invoice charged immediately. Failure →
incomplete(expires after 24h). - Trial:
trialing; first invoice attrial_end. - Renewals: invoice at
next_billing_at, mandate re-verified, charge executed. - Failures: typed reason, retry at +1/+3/+5 days,
past_dueafter the first failure,invoice.failedwebhook, thencanceled/unpaidper plan policy. max_cyclesreached →expired.
Endpoints
Customer cancel is manual: cancel-at-period-end is confirmed in a Tribridge modal, then the subscription cancels at the boundary.
Webhooks
Reuses the existing HMAC-SHA256 delivery and retry pipeline (X-Tribridge-Signature).
subscription.created, subscription.active, subscription.trial_will_end, subscription.past_due, subscription.paused, subscription.resumed, subscription.canceled, subscription.expired, invoice.upcoming, invoice.paid, invoice.failed, mandate.needs_reauthorization.
Payloads include subscription_id, invoice_id, plan_id, livemode (as is_test), and a data object with the full subscription/plan/invoice snapshot.
Fees and ledger
The existing platform fee is used (max($0.10, 1%)). The invoice stores gross, fee and net; the ledger credits the merchant net through a completed payments row, so payouts and balances work unchanged. Subscription charges appear in Transactions with source subscription, and lifecycle events appear in Logs → Subscription.
Test mode
Test subscriptions arelivemode=false and always use the sandbox adapter:
insufficient_balance, allowance_exhausted, delegate_revoked, account_closed, rpc_error, ambiguous. Send {"failure":null} to clear.
Mainnet requirements (Solana)
Required backend env:Refunds
Normal automatic refunds cover underpaid, overpaid and wrong-token deposits. If funds arrived on-chain but the payment was not recorded: a super admin can open Admin → Payments → payment drawer → Refund unrecorded payment. It refunds the actual deposit-wallet balance to the payer, signs a real transaction, marks the paymentrefunded, and writes an audit log. It refuses completed/test payments and zero balances.
