Billing via TOLL: Decision Accepted, Polar Still in Code
smo1 intermediate 8 min read
ELI5
On 2026-10-24 the owner decided that smo1 will sell through TOLL, the estate’s one payment rail, and that purr-api’s direct Polar integration goes away. That decision is signed. The code has not caught up: every billing route still calls Polar directly, and the Polar webhook is still the only thing that changes a user’s plan. A small TOLL client exists in the code but nothing uses it yet. Both statements are true at once, and this card exists so nobody mistakes one for the other.
Technical Deep Dive
Source: purr-api docs/decisions/2026-10-24-sell-through-toll.md (status accepted, countersigned flight@arno.host 2026-10-24, commit 81aa364), cmd/server/main.go:217-247, 314-318, internal/router/router.go:142-144, 217-227, internal/service/billing_service.go:99-108, 528-574, internal/service/billing_events.go:28-38, internal/billing/toll/client.go:18-20, 91-140, internal/config/config.go:39-43, go.mod:14 (origin/main bb6cfc2); smo1-io .petrova/gates.yaml:171-196, docs/decisions/2026-10-15-polar-billing-plan-change-and-refund.md (accepted), docs/findings/20261020-1529-launch-readiness-gates-evidence.md (1f19667), read 2026-10-25.
Truth 1: the decision (accepted)
| Ruling | Content |
|---|---|
| 1 | Every billing call (checkout, plan change, cancel, reactivate, portal, billing-info read) goes through TOLL; polar-go, the Polar webhook route and handler, Polar event processing and every POLAR_* setting are removed |
| 2-5 | users.tier stays as a cache of TOLL entitlements with refresh, fail-safe and early_adopter rules (smo1-026) |
| 6 | Dev mode stays: with no TOLL configured, billing actions mutate local state |
| 7 | Polar columns and the payments / revoked_subscriptions tables go dormant; dropping them is a separate operator-gated migration |
It depends on null0-toll/toll’s subscription-lifecycle-verbs decision and its implementation, TOLL#166. Operator actions listed before the Polar-removal merge: TOLL#166 deployed to the sandbox gate, TOLL_URL, TOLL_KEY_ID, TOLL_KEY_SECRET, TOLL_ENV set on Railway, POLAR_* removed after a verified deploy, Polar webhook endpoint disabled.
Truth 2: the running code (Polar)
sequenceDiagram autonumber actor U as User participant M as meow-web participant P as purr-api participant Pol as Polar U->>M: Upgrade M->>P: POST /api/purr/billing/checkout P->>Pol: polar-go create checkout Pol-->>P: hosted checkout URL P-->>U: redirect to Polar Pol->>P: POST /api/webhooks/polar, Standard Webhooks signed P->>P: verify webhook-id, timestamp, signature P->>P: dedupe on webhook-id, then dispatch event P->>P: write users.tier and subscription columnsmain.go:222-247 buildsBillingServicefromPOLAR_ACCESS_TOKEN,POLAR_WEBHOOK_SECRET, fourPOLAR_*_PRICE_IDs andPOLAR_SERVER, then starts a background product check.go.mod:14 pinspolar-go v0.12.0.- Routes (
router.go:217-227):GET /api/billing,POST /billing/checkout,/change-plan,/cancel,/reactivate,GET /billing/portal,GET /billing/payments; the webhookPOST /api/webhooks/polaris public (142-144). - Handled events (
billing_events.go:28-38):subscription.created,.active,.uncanceled,.updated,.past_due,.canceled,.revoked,order.paid,order.refunded. - The TOLL client (
internal/billing/toll, purr-api#77) hasCheckout,Entitlements,TierFromCapabilities(smo1.business>smo1.pro> free) andProductKey. No non-test file imports it;TOLL_*config fields exist and are documented as “optional until the call sites switch” (config.go:39-43). Branchesfeat/sell-through-tollandfeat/toll-entitlement-sourceexist on origin without merged PRs for the wiring.
Mode selection
stateDiagram-v2 [*] --> polar : POLAR_ACCESS_TOKEN set [*] --> devfake : no token and kitten dev mode [*] --> unconfigured : no token and not dev polar : polar mode = POLAR_SERVER value devfake : dev-fake, billing mutates local state unconfigured : routes registered only if repos existPolarMode() returns the server name, dev-fake or unconfigured (billing_service.go:99-108) and is logged at boot. Which mode production runs in today is UNVERIFIED: the session ledger’s 2026-10-10 read-only Railway check found no POLAR_* variables in staging or production, and the later sandbox gate runs do not say which environment carried the sandbox token.
Launch gates still assume Polar
Gate G-5-1 (payment reconciles end to end) was met on 2026-10-11 to 14 with a Polar sandbox checkout and refund. Gate G-5-12 is “Polar live mode”, blocked on G-5-1 to G-5-11 and on the seven POLAR_* production variables (.petrova/gates.yaml:171-196), and the gates file’s target is “first live Polar payment reconciled”. The accepted TOLL decision removes those variables, so G-5-12 as written can no longer be the path to revenue. No record re-targets the gates file yet (smo1-029).
Key Terms
- TOLL — the estate payment rail; apps call verbs (
POST /verb/<name>) and read entitlements - Sell-through — purr-api calls TOLL for every billing action instead of Polar
- Standard Webhooks — the signing scheme Polar uses:
webhook-id,webhook-timestamp,webhook-signature - dev-fake — billing mode in kitten without Polar; actions mutate local state
- G-5-12 — the “Polar live mode” launch gate the TOLL decision overtook
Q&A
Q: What payment provider does SMO1 use?
A: In running code, Polar, called directly through polar-go with a Polar webhook as the only writer of users.tier. By accepted decision (2026-10-24), TOLL, which itself fronts Polar; the switch is not implemented yet.
Q: How does the system prevent duplicate webhook processing? A: It verifies the Standard Webhooks signature over id, timestamp and body, then deduplicates on the webhook id before dispatching, per the 2026-10-09 Polar billing contract and its 2026-10-15 amendment (accepted). Under the TOLL decision this webhook is deleted and TOLL owns idempotency.
Q: What happens in development when Polar is not configured?
A: With no Polar token and dev mode on, PolarMode() is dev-fake and billing actions change local state so the dashboard can be exercised. Ruling 6 keeps the same behaviour when TOLL is not configured.
Q: Which launch gate did the TOLL decision overtake, and what does the gates file still say?
A: G-5-12, “Polar live mode”. .petrova/gates.yaml still blocks it on seven POLAR_* production variables and still names a live Polar payment as the target, while the accepted decision removes every POLAR_* setting.
Examples
A user upgrades today: purr-api creates a Polar checkout directly, and the tier changes only when Polar’s webhook reaches /api/webhooks/polar. After the sell-through lands, the same click calls toll.checkout with product key smo1_pro_monthly, and the tier changes when purr-api next reads toll.entitlements: on the billing page, after a lifecycle action, or on the periodic sweep (smo1-026).
neighbors on the map
- Core Data Models designing a new feature that touches users, links, or achievements
- Rate Limiting: Edge per-IP, Origin per-Tier debugging 429 errors for specific users
- x402 Paid Anonymous Links: One Payment Buys One Link working on POST /api/links or anything in internal/x402