Skip to content

E6 — Monetization (ALO-125)#111

Open
aloewright wants to merge 1 commit into
mainfrom
conductor/alo-125-e6-monetization
Open

E6 — Monetization (ALO-125)#111
aloewright wants to merge 1 commit into
mainfrom
conductor/alo-125-e6-monetization

Conversation

@aloewright
Copy link
Copy Markdown
Owner

@aloewright aloewright commented May 8, 2026

Closes ALO-125 — https://linear.app/aloey/issue/ALO-125/e6-monetization

Summary

  • Adds Polar (Merchant of Record) as Spooool's payments spine: creator partner onboarding, channel memberships, per-video tipping, and a creator payouts dashboard.
  • Webhook-driven D1 ledger (monetization_ledger) is append-only and idempotent on polar_event_id, so retried Polar deliveries never double-credit.
  • Spooool's revenue share is configurable in basis points (POLAR_PLATFORM_FEE_BPS, default 1000 = 10%) and recorded per ledger row, so future rate changes don't rewrite history.

Files

  • src/db/migrations/0019_monetization.sqlcreator_polar_accounts, membership_tiers, memberships, polar_events (raw audit), monetization_ledger, payouts.
  • src/workers/polar.ts — thin Polar REST client + Standard-Webhooks signature verification (handles whsec_<base64> and plain secrets, multi-sig rotation tolerance).
  • src/workers/polar-webhook.ts/api/webhooks/polar receiver. Persists every payload to polar_events, dispatches into ledger / membership / payout / account state. Failures recorded as process_error rather than retried indefinitely.
  • src/workers/monetization.ts — creator + viewer surface (see Routes below).
  • src/workers/index.ts, wrangler.toml — wiring + Doppler secret docs.

Routes

  • POST /api/monetization/onboarding/start — creator → Polar partner link
  • GET /api/monetization/onboarding/status — onboarding state
  • POST /api/monetization/tiers · DELETE /api/monetization/tiers/:tierId — creator tier CRUD
  • GET /api/monetization/channels/:username/tiers — public tier list
  • POST /api/monetization/memberships/checkout — viewer → Polar Checkout (recurring)
  • POST /api/monetization/tips/checkout — viewer → Polar Checkout (one-time)
  • GET /api/monetization/me/memberships — viewer dashboard
  • GET /api/monetization/me/payouts — creator dashboard (totals + ledger + payouts)
  • POST /api/webhooks/polar — signed webhook (CSRF-exempt via existing /api/webhooks/* rule)

Secrets to set (Doppler / wrangler secret put)

  • POLAR_ACCESS_TOKEN — polar.sh API token
  • POLAR_ORGANIZATION_ID — Polar org id used as parent for products + partners
  • POLAR_WEBHOOK_SECRET — Standard Webhooks signing secret (whsec_<base64> or plain)
  • POLAR_PLATFORM_FEE_BPS — optional, defaults to 1000 (10%)
  • PUBLIC_ORIGIN — public origin for Polar return / cancel URLs

Test plan

  • npm test — 533 tests, 47 files passing; 42 new tests cover signature verification, fee math, ledger idempotency, membership upsert, payout recording, account state transitions, and the auth/lifecycle gates on every monetization route
  • npm run lint clean (oxlint + AI Gateway provider guard)
  • npm run type-check clean
  • npm run build clean
  • Apply migration in staging: npm run db:migrate:staging
  • Configure Polar webhook endpoint to https://<env>/api/webhooks/polar and copy the signing secret into POLAR_WEBHOOK_SECRET
  • End-to-end: creator onboarding → publish membership tier → buyer checkout → webhook lands ledger row → dashboard reflects pending earnings → Polar payout webhook records the payout

Out of scope (deferred per ticket)

Ads, pay-per-view rentals.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 8, 2026 15:00
@aloewright aloewright added the conductor Conductor-managed PR label May 8, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Warning

Rate limit exceeded

@aloewright has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 25 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b776bfb1-d5af-4bb2-9b81-147144f366a0

📥 Commits

Reviewing files that changed from the base of the PR and between 4d3c13f and 3f44919.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • src/db/migrations/0019_monetization.sql
  • src/workers/index.ts
  • src/workers/monetization.test.ts
  • src/workers/monetization.ts
  • src/workers/polar-webhook.test.ts
  • src/workers/polar-webhook.ts
  • src/workers/polar.test.ts
  • src/workers/polar.ts
  • wrangler.toml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch conductor/alo-125-e6-monetization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ecc-tools
Copy link
Copy Markdown
Contributor

ecc-tools Bot commented May 8, 2026

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6bfd41e934

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/workers/polar.ts Outdated
Comment on lines +300 to +304
// The Polar product is created on-demand at first checkout; here we
// record a local placeholder so the channel page can list tiers
// immediately. The webhook reconciles the polar_product_id when the
// first purchase completes.
const id = crypto.randomUUID();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Create Polar catalog products before membership checkout

/api/polar/memberships stores a random UUID as polar_products.id and never creates a corresponding product in Polar, but /api/polar/checkout/membership later sends that ID to createCheckout as product_id. Polar checkout sessions require an existing Polar product ID, so newly created memberships will fail at checkout time (typically 4xx from Polar) and buyers cannot complete purchases.

Useful? React with 👍 / 👎.

Comment thread src/workers/polar.ts Outdated
Comment on lines +380 to +383
const tipId = crypto.randomUUID();
await c.env.DB.prepare(
`INSERT INTO polar_products (id, user_id, kind, name, price_cents, currency)
VALUES (?, ?, 'tip', ?, ?, 'USD')`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop using local UUIDs as Polar product IDs for tips

The tip checkout flow inserts a local tipId into polar_products and immediately passes that UUID to Polar createCheckout as productId, but no Polar product is created first. In this path, every tip checkout depends on a non-existent remote product, so tip payments will fail before a customer can pay.

Useful? React with 👍 / 👎.

Comment thread src/workers/polar.ts Outdated
Comment on lines +557 to +560
const billingTypes = new Set([
'order.created',
'order.paid',
'subscription.created',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record only paid orders in the earnings ledger

Including order.created as a billable event writes ledger rows before payment is finalized; Polar documents that order.created can be pending. Because inserts are idempotent on polar_order_id, the later order.paid event for the same order is dropped, leaving unpaid/pending orders permanently counted in creator pending/lifetime payout totals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…ts (ALO-125)

Adds the E6 monetization spine:

- D1 migration 0019 introduces creator_polar_accounts, membership_tiers,
  memberships, polar_events, monetization_ledger, and payouts. The ledger
  is append-only and keyed on polar_event_id so retried webhook
  deliveries no-op safely.
- src/workers/polar.ts is a thin Polar REST client plus Standard-Webhooks
  signature verification (handles whsec_<base64> and plain secrets, and
  multi-sig rotation). Spooool's revenue share is configurable in basis
  points via POLAR_PLATFORM_FEE_BPS (default 1000 = 10%).
- src/workers/polar-webhook.ts receives signed events at
  /api/webhooks/polar, persists every payload to polar_events for audit,
  and dispatches into the canonical ledger plus derived membership and
  payout state. Failures are recorded as process_error rather than
  retried indefinitely.
- src/workers/monetization.ts exposes the creator + viewer surface:
  partner onboarding, membership tier CRUD, public tier listing,
  Polar Checkout for memberships and tips, viewer membership list, and
  the creator payouts dashboard with totals + ledger + payout history.
- All routes are wired into the main worker; CSRF is bypassed for
  /api/webhooks/* as before. Doppler secrets are documented in
  wrangler.toml.

Tests cover signature verification, fee math, ledger idempotency,
membership upsert, payout recording, account state transitions, and the
auth/lifecycle gates on every monetization route. 533 vitest tests pass;
type-check, oxlint, and the AI-Gateway provider guard are all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aloewright aloewright force-pushed the conductor/alo-125-e6-monetization branch from 6bfd41e to 3f44919 Compare May 9, 2026 05:18
@ecc-tools
Copy link
Copy Markdown
Contributor

ecc-tools Bot commented May 9, 2026

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conductor Conductor-managed PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants