You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracking issue for two credit-billing changes motivated by the 2026-07-08 auto-recharge incident: a long-tenured free-tier customer's card was charged $5.46 off-session (refunded, re_3TqUw700JObOnOb51S1XHMKD) after platform-side changes repriced his year-old scheduled task; $5.70 of his $12.51 14-day spend was flat 5-credit research fees. Business context (out of scope here): reduce surprise-charge churn; the incident write-up lives in Attio on the customer's record.
Status: DONE 2026-07-09 — all three PRs merged (docs → api → chat) and live on prod; both goals verified end-to-end including a real-card charge sequence.
2026-07-08 decision (Sweets): the opt-out flag lives in Stripe customer metadata, not a database column — Stripe is the source of truth for charges and payment details, so consent sits next to the card it governs. Supersedes the credits_usage column suggestion below (kept struck-through for the record). Key consequence: card-on-file and charge-consent are decoupled — an opted-out customer can still top up manually via Checkout (which saves their card), and opting back in later is a metadata flip, no card re-entry.
Goal
POST /api/research/web deducts 1 credit per call instead of 5. We pay Perplexity Search API $0.005/request ($5 per 1,000, flat, no token costs) — the current 5-credit fee is a 10x markup that punishes exactly the high-frequency agentic search we want. All other research-family pricing is unchanged (Songstats-backed endpoints stay at 5 — they cost ~$0.09/call amortized on the Professional plan and are already margin-negative; deep stays 25; enrich/extract unchanged).
Customers can opt out of automatic top-up from the chat settings UI. Today docs/credits.mdx L134 says per-account control "is not yet exposed — reach out to support". Opted-out accounts fail closed to the existing explicit-consent path (402 + checkoutUrl), never a silent off-session charge.
✅ merged 2026-07-09 (c922691) — fully preview-verified incl. live auto-recharge sequence with a real card (endpoint checks, charge sequence); GET read-after-first-write lag (~2.5 min, card-less accounts only) flagged for chat#1862
Automatic top-up switch in a Billing section of the Account modal (live Stripe-read state + $5/500-credit helper text + error toast; redesigned from the dropdown checkbox per review 2026-07-09)
Merge sequencing: docs → api → chat. The docs change is the pricing/opt-out contract the rest fulfills; the chat toggle calls the new api endpoint. database migration dropped 2026-07-08 — no schema change needed with the Stripe-metadata design. All three merged 2026-07-09; both goals live on prod.
Done
Reprice POST /api/research/web from 5 credits to 1 (docs#270 + api#769).
✅ Shipped 2026-07-09 (both merged to main: docs 300fc0a, api c922691).
Docs cost-table row 5 → 1; api gives /research/web its own gate (ensureWebResearchCredits, 1 credit) and postResearchWebHandler deducts 1 — the shared RESEARCH_CREDIT_COST is untouched, so the Songstats-backed family still gates and deducts 5.
Verified live on the PR-head preview 2026-07-09: request succeeds at exactly remaining_credits = 1 and writes usage_events.credits_deducted_cents = 1 (16:26:06 UTC row); 402 at 0 balance carries required_credits: 1; sibling still deducts exactly 5 (332 → 327) — the sibling exercised was GET /api/research?q=… (search) rather than the criterion's GET /api/research/metrics (metrics needs a rostered artist fixture); both ride the same shared 5-credit handleResearch path (endpoint checks, charge sequence).
Self-serve opt-out of automatic top-up (chat settings UI + api + flag) (docs#270 + api#769 + chat#1862).
✅ Shipped 2026-07-09 (all merged to main in contract order: docs 300fc0a → api c922691 → chat 3dccdae; chat prod deployment success).
Flag = Stripe customer metadata key auto_recharge_opt_out (presence = opted out; re-enabling deletes the key; resolved via resolveStripeCustomerForAccount, never email). autoRechargeOrFail does a fresh customers.retrieve check before any off-session charge and falls closed to the existing 402 + checkoutUrl. Surface: GET/PATCH /api/accounts/{id}/auto-recharge + an Automatic top-up switch in a Billing section of the chat Account modal — redesigned 2026-07-09 from the original dropdown checkbox (Sweets: a billing-consent control needs explanation a menu item can't carry) with $5/500-credit helper text and an error toast. database migration dropped 2026-07-08 (Stripe-metadata decision, see callout). Verified live 2026-07-09, including a real card: enabled + card + 0 balance → silent $5.34 off-session top-up fired and the request succeeded (balance 0 → 499); opted-out + chargeable card + 0 balance → 402 + checkoutUrl, no PaymentIntent, balance stayed 0; re-enable needed no card re-entry (charge sequence, endpoint matrix). Chat UI round-trip + reload persistence verified on the PR preview against the synced test-recoup-api (screenshots).
Known accepted wrinkle: the GET reads the customer via Stripe's eventually-consistent Search index, so the first-ever opt-out on a card-less account can read back true for ~1–2.5 min across a reload (safety unaffected — the charge gate reads fresh; the UI seeds its cache from the PATCH response so it doesn't bounce in-session).
Done when: with the toggle off and remaining_credits below a request's cost, a billed request produces a 402 with checkoutUrl and no off-session PaymentIntent with metadata.purpose = credits_auto_recharge is created (manual Checkout PIs remain possible and desired — that's the escape hatch); completing that Checkout saves the card yet subsequent low-balance requests still 402 while the flag is present; toggling back on (key deleted) restores today's silent top-up without re-entering card details; the toggle reflects Stripe state across reloads and a toggle-off is honored by the very next request (fresh-read rule).
Architecture decisions
Per-tool pricing, not a flat research fee. The 5-credit default conflates two cost bases: Perplexity web search ($0.005/call, flat) and Songstats-backed catalog research (~$0.09/call amortized). Web search is repriced alone; touching the shared RESEARCH_CREDIT_COST would silently reprice the negative-margin family too.
Opt-out fails closed to explicit consent, not to silent failure. Opted-out + insufficient balance reuses the existing 402 + checkoutUrl contract (credits.mdx L161) rather than inventing a new error shape — clients already handle it.
Consent lives in Stripe customer metadata (decided 2026-07-08, supersedes the DB-column plan). Stripe is the source of truth for charges and payment details; the flag sits next to the card it governs, support can flip it from the Stripe dashboard with no deploy, and customer.updated events give an audit trail. Charge path must read it fresh (customers.retrieve), not from the eventually-consistent search index.
Default stays opt-in (enabled). Existing behavior is unchanged for accounts that don't touch the toggle; this is a kill switch, not a default flip.
Accepted regressions / tradeoffs
Consent doesn't port off Stripe. The x402/crypto payment path doesn't ride Stripe; if top-ups ever go multi-processor, the flag needs a per-processor home or a migration to our DB. Fine today — auto top-up is Stripe-only.
A deleted/recreated Stripe customer silently resets to opted-in (metadata doesn't survive recreation). Rare; mitigated by the toggle always rendering live Stripe state, so the UI never lies about current consent.
Tracking issue for two credit-billing changes motivated by the 2026-07-08 auto-recharge incident: a long-tenured free-tier customer's card was charged $5.46 off-session (refunded,
re_3TqUw700JObOnOb51S1XHMKD) after platform-side changes repriced his year-old scheduled task; $5.70 of his $12.51 14-day spend was flat 5-credit research fees. Business context (out of scope here): reduce surprise-charge churn; the incident write-up lives in Attio on the customer's record.Status: DONE 2026-07-09 — all three PRs merged (docs → api → chat) and live on prod; both goals verified end-to-end including a real-card charge sequence.
Goal
POST /api/research/webdeducts 1 credit per call instead of 5. We pay Perplexity Search API $0.005/request ($5 per 1,000, flat, no token costs) — the current 5-credit fee is a 10x markup that punishes exactly the high-frequency agentic search we want. All other research-family pricing is unchanged (Songstats-backed endpoints stay at 5 — they cost ~$0.09/call amortized on the Professional plan and are already margin-negative;deepstays 25;enrich/extractunchanged).docs/credits.mdxL134 says per-account control "is not yet exposed — reach out to support". Opted-out accounts fail closed to the existing explicit-consent path (402 +checkoutUrl), never a silent off-session charge.PRs (updated 2026-07-09)
main/research/webgate + deduction → 1 credit; Stripe-metadata opt-out inautoRechargeOrFail(fresh retrieve);GET/PATCH /api/accounts/{id}/auto-rechargemainAutomatic top-upswitch in a Billing section of the Account modal (live Stripe-read state + $5/500-credit helper text + error toast; redesigned from the dropdown checkbox per review 2026-07-09)mainsuccessDone
Reprice
POST /api/research/webfrom 5 credits to 1 (docs#270 + api#769).✅ Shipped 2026-07-09 (both merged to
main: docs 300fc0a, api c922691).Docs cost-table row 5 → 1; api gives
/research/webits own gate (ensureWebResearchCredits, 1 credit) andpostResearchWebHandlerdeducts 1 — the sharedRESEARCH_CREDIT_COSTis untouched, so the Songstats-backed family still gates and deducts 5.Verified live on the PR-head preview 2026-07-09: request succeeds at exactly
remaining_credits = 1and writesusage_events.credits_deducted_cents = 1(16:26:06 UTC row); 402 at 0 balance carriesrequired_credits: 1; sibling still deducts exactly 5 (332 → 327) — the sibling exercised wasGET /api/research?q=…(search) rather than the criterion'sGET /api/research/metrics(metrics needs a rostered artist fixture); both ride the same shared 5-credithandleResearchpath (endpoint checks, charge sequence).Self-serve opt-out of automatic top-up (chat settings UI + api + flag) (docs#270 + api#769 + chat#1862).
✅ Shipped 2026-07-09 (all merged to
mainin contract order: docs 300fc0a → api c922691 → chat 3dccdae; chat prod deploymentsuccess).Flag = Stripe customer metadata key
auto_recharge_opt_out(presence = opted out; re-enabling deletes the key; resolved viaresolveStripeCustomerForAccount, never email).autoRechargeOrFaildoes a freshcustomers.retrievecheck before any off-session charge and falls closed to the existing 402 +checkoutUrl. Surface:GET/PATCH /api/accounts/{id}/auto-recharge+ anAutomatic top-upswitch in a Billing section of the chat Account modal — redesigned 2026-07-09 from the original dropdown checkbox (Sweets: a billing-consent control needs explanation a menu item can't carry) with $5/500-credit helper text and an error toast.database migrationdropped 2026-07-08 (Stripe-metadata decision, see callout).Verified live 2026-07-09, including a real card: enabled + card + 0 balance → silent $5.34 off-session top-up fired and the request succeeded (balance 0 → 499); opted-out + chargeable card + 0 balance → 402 +
checkoutUrl, no PaymentIntent, balance stayed 0; re-enable needed no card re-entry (charge sequence, endpoint matrix). Chat UI round-trip + reload persistence verified on the PR preview against the syncedtest-recoup-api(screenshots).Known accepted wrinkle: the GET reads the customer via Stripe's eventually-consistent Search index, so the first-ever opt-out on a card-less account can read back
truefor ~1–2.5 min across a reload (safety unaffected — the charge gate reads fresh; the UI seeds its cache from the PATCH response so it doesn't bounce in-session).remaining_creditsbelow a request's cost, a billed request produces a 402 withcheckoutUrland no off-session PaymentIntent withmetadata.purpose = credits_auto_rechargeis created (manual Checkout PIs remain possible and desired — that's the escape hatch); completing that Checkout saves the card yet subsequent low-balance requests still 402 while the flag is present; toggling back on (key deleted) restores today's silent top-up without re-entering card details; the toggle reflects Stripe state across reloads and a toggle-off is honored by the very next request (fresh-read rule).Architecture decisions
RESEARCH_CREDIT_COSTwould silently reprice the negative-margin family too.checkoutUrlcontract (credits.mdxL161) rather than inventing a new error shape — clients already handle it.customer.updatedevents give an audit trail. Charge path must read it fresh (customers.retrieve), not from the eventually-consistent search index.Accepted regressions / tradeoffs
Source references
ch_3TqUw700JObOnOb51S1XHMKD(2026-07-08 refund), account205dea81-af4f-4700-937c-c7031cb7de86usage ledger 2026-06-25 → 2026-07-08.web_fetchempty-body bug — the tool-layer sibling of this pricing change), Chartmetric API suspended (ToS) — migrate /research to Songstats #1777 (Chartmetric → Songstats migration; owns the Songstats-side economics).