Product changelog
What we shipped, version by version
Hand-written by the engineering team. Every entry maps to a real feature, fix, or security update — no marketing language, just facts.
- v0.31Improved
Your plan’s account count now applies across the product
The dashboard, audits, exports, AI recommendations and the public API now work with the accounts your plan covers, like the ads manager (earliest connections first, TikTok, Meta and Google together).
- Accounts beyond your plan stay connected; the dashboard shows a note, and they are included once you upgrade or disconnect others
- The public API adds plan_account_limit to /v1/me, and /v1/accounts lists only the accounts your plan covers
- v0.30Improved
New pricing by number of ad accounts, from $10
Starter $10 (1 ad account), Personal $39.99 (up to 20), Standard $59.99 (up to 40), Enterprise $99.99 (up to 80); 20% off yearly and a 7-day free trial on every plan. Each plan’s AI generations, AI videos, visibility checks and creator searches are on the pricing page.
- The ads manager works with as many accounts as your plan covers: when more are connected, the earliest connections are managed and the rest stay connected, with a note to upgrade or disconnect
- Subscribers on Professional or Business keep their plan and quotas
- Affiliate program: 10% standard, up to 20% for selected partners, no tier ladder, instant approval, paid on subscription payments only (see the affiliate page)
- v0.29New
Creating in several accounts now uses the five steps too, with each account’s own ads
“In several accounts at once” now has the five steps: the objective, campaign and ad group settings (except custom audiences) are shared by every account you tick, ads are chosen per account, and each account has its own result.
- Each account can use its own TikTok posts, videos in that account or a video link — or only get the campaign and ad group
- An ad that was not created can be corrected and retried in the results; “Past batches” turns a batch on or off. Ads are created on and start when their campaign and ad group are turned on
- v0.28New
Create a complete TikTok campaign in five steps; edit budget, bid and countries from the ad list
Type → campaign → ad group → creatives → review: one submit creates the campaign, the ad group and several ads; the ad list now edits names, budgets, bids and countries.
- Full ad group settings: bid strategy and pacing, automatic or chosen placements (TikTok, Pangle, Global App Bundle), gender / age / language / OS, custom audiences, interests and behaviours, budget and schedule, dayparting, frequency, comments off; campaign budget optimization and special ad categories on the campaign
- Several ads per submit: a TikTok post, a video already in the account or a video link; lead ads open an instant form; an ad that was not created can be retried on its own
- Ad list “Edit”: only changed values are sent and other targeting stays as it is; TikTok’s refusal is shown word for word and the list then shows TikTok’s current values
- v0.27New
TikTok posts and Spark Ads: asset library, authorization codes, ads from posts
The asset library now takes TikTok accounts’ posts and creators’ authorized posts; a new Spark Ads page applies authorization codes in bulk; ads can be created straight from a post.
- Asset library “TikTok posts”: video posts of linked and Business Center TikTok accounts and creators’ authorized posts; pushing a creator’s post applies its code to the target, other posts are checked for the target and the reason is shown
- Spark Ads page: apply authorization codes to many ad accounts (a background task; codes stored encrypted and shown masked), see and unbind authorized posts
- Create ads from “TikTok post (Spark Ads)”: no upload, created paused; TikTok’s refusal is shown word for word
- v0.26NewSecurity
Ad management console: one list across accounts, on/off switches, cross-account copy, asset library and teams
See and manage ads across your TikTok and Meta ad accounts in one place. Every change happens only when you click and confirm, new and copied campaigns start paused, and the AI never changes your account on its own.
- Ad list: campaigns, ad groups and ads across accounts in one list, filtered by date range and status, sortable, with on/off switches (TikTok and Meta)
- Copy: TikTok campaigns (including Smart+) into the same or other connected accounts, 1–5 copies at a time; ad groups within an account; Meta campaigns and ad sets. Copies start paused; failed items can be retried
- Templates and tags: save the settings you reuse; tag accounts and pick by tag; a task log records the result of every step
- Asset library: pick videos and images from connected TikTok accounts and push them into up to 50 accounts
- Open ad accounts in your own TikTok Business Center (needs Business Center admin rights)
- Teams: invite people as viewer, operator or admin to work in your workspace
- Account cards show the TikTok authorization state: an account whose access was revoked says so, with a "Re-authorize TikTok" button
- Security: limits on sign-in attempts; changing or resetting your password signs out other devices; actions sent from other websites using your sign-in are refused
- v0.25CoreImprovement
Trust upgrades: 95% confidence interval on savings + Founding Member Beta + AI cost guard
This release is almost entirely about one thing: making the numbers we show you trustworthy. The savings headline now carries a 95% confidence interval underneath, turning "looks precise" into "honestly precise". The Founding Member Beta landing page is live with the ROI money-back guarantee (final terms pending counsel review). The two greyed-out buttons on AI suggestion cards are now a clear "Advisory" tag so they no longer look broken. Behind the scenes we added result caching (same data → instant + free) and a generous monthly fair-use ceiling that only catches runaway scripts — real customers never see it.
- The "Saved this month $X,XXX" headline now shows a 95% confidence interval underneath (e.g. $900 – $1,560), computed by propagating the persisted Bayesian ROAS posterior through the counterfactual via error propagation (the only uncertain term in net savings is counterfactualSpend × predictedRoas). Variances sum across independent changes. New lib/savings-tracker/interval.ts with 10 unit tests covering σ recovery, variance summation vs SE, string / missing-field degradation. Upgrades the headline from a marketing point estimate to a defensible statistic with honest uncertainty
- "Building baseline · X / 5 measurements" indicator for small accounts. When the month has 1-4 AI changes (not yet a stable estimate), a small amber chip tells you the number will firm up as more measurements accumulate, instead of dropping a bare figure on you. Zero changes still uses the existing onboarding card (connect accounts + let the AI run 7 days)
- /[lang]/beta Founding Member landing page live (en / zh-CN / es), 50 founding spots, anchored on the ROI money-back guarantee: annual plans — if our holdout-measured savings come in below the subscription fees you paid, we refund the difference, capped at 100% of fees. Slogan "We prove your savings — or your subscription is free" lands on the homepage + dashboard top once counsel signs off on the final terms
- AI suggestion cards: the two permanently-disabled Dismiss / Apply buttons are replaced by a clear "Advisory" badge per card plus a one-line panel note: "These are advisory suggestions — review and apply them in your ad platform." Honest framing, reads as intentional rather than broken
- AI suggestion result caching: identical input (data snapshot + provider preference) returns the cached result within a 12h TTL — no LLM call, zero token cost. The cache key changes the moment the data changes (new day, new connected account) or the provider preference switches, forcing a fresh generation. New ai_usage_events table serves both jobs (cache source + monthly counter); every DB call fails OPEN so a database hiccup never blocks a customer's recommendations
- Generous monthly fair-use ceiling — 200 free/trial, 600 Professional, 3000 Business per month — all sit comfortably above the heaviest modelled real usage (~150/month). Only catches runaway scripts. Hitting it shows a friendly "data only refreshes daily, email [email protected] and we'll raise your limit" message, never a hard error or an anxiety-inducing usage meter
- Admin / review backend pinned to English + Chinese only (internal operator tool — no Spanish needed). Fixes a small bug where /es/admin previously rendered the "last seen" relative time in Chinese (relativeTime only supports en/zh, and a Spanish admin falling back to Chinese was nonsensical); now consistently English
- Tests 767 → 788 (+21 new, covering the CI math + 3 cost-guard route paths: cache hit / over-cap / real-generation persisted). Lint clean, build green, zero production errors. Version 0.24.0 → 0.25.0
- v0.24CorePlatform
Shadow mode live: optimizer wired to persistence + daily cron + outcome measurement
v0.22-0.23 proved the math works. v0.24 wires it into the production loop — the moment Google Ads dev token approval lands, everything just works. Added lib/ads-fetcher platform abstraction (mock + real-platform stubs), lib/budget-optimizer/persistence (recordRecommendation + recordOutcome + getOrAssignHoldout + daily aggregation), /api/cron/run-optimizer (daily optimizer pass writing budget_changes in shadow mode — never touches real platform budgets yet), /api/cron/measure-outcomes (looks up actual platform metrics 7 days later, computes counterfactual savings, writes change_outcomes). The dashboard SavingsHero now reads real DB rows — empty state when no AI activity, real savings number when there is. End-to-end pipeline: fetcher → optimizer → persist → measure → dashboard.
- lib/ads-fetcher abstraction: mockAdsFetcher (uses MOCK_ACCOUNTS + deterministic noise per (campaignId, windowEnd)) + googleAdsFetcher / metaAdsFetcher / tiktokAdsFetcher stubs (throw with actionable migration message) + getAdsFetcher router checks USE_REAL_ADS_FETCHER env. 13 test paths cover router default, "1" flipping, accidental-on prevention, mock determinism, snapshot ordering, unknown-account null, three-platform stub error messages
- lib/budget-optimizer/persistence: getOrAssignHoldout reads first, falls back to deterministic compute on race; recordRecommendation writes budget_changes (numeric values via .toFixed for precision); recordOutcome writes change_outcomes; listChangesAwaitingOutcome left-joins for unmeasured changes; listChangesForUser/aggregateUserSavings/dailySavingsForUser feed dashboard. Every DB call wrapped in try/catch + structured logger.error so cron survives transient DB hiccups
- /api/cron/run-optimizer: CRON_SECRET Bearer auth → pulls all non-revoked / non-token-expired ad_accounts → per ad_account fetches 8 weeks of history → per campaign assigns/reads holdout (control campaigns skipped early) → recommend() → persists active recommendations as budget_changes in shadow mode (hold/insufficient_data don't pollute the table). Per-ad-account try/catch with captureError so one bad account doesn't kill the whole sweep, structured logger.info summary at end
- /api/cron/measure-outcomes: finds budget_changes ≥ windowDays old without outcomes → fetcher pulls actual spend/revenue for that window → computeOutcome calculates counterfactual netSavings → writes change_outcomes. Configurable via OUTCOME_WINDOW_DAYS env (default 7), capped at 500 changes per run. Records zero-spend outcome when campaign no longer exists (defends against infinite retry)
- Dashboard SavingsHero now reads real DB: loadSavingsForUser parallel-queries month-to-date / trailing-7d / lifetime aggregates + 30-day daily series. When changeCount=0 the hero auto-renders the empty state (already baked in since v0.22), real outcomes drive real number + chart (negative days included honestly). End-to-end loop closes: cron → DB → dashboard
- Tests 644 → 657 (+13 ads-fetcher paths). 0 lint warnings, build green. Version 0.23.0 → 0.24.0
- Production-critical path tests filled in (+37 new tests). lib/budget-optimizer/persistence gets 16 paths covering getOrAssignHoldout (existing / new / control routing / DB-down fallback), recordRecommendation success + failure, listChangesForUser numeric→number conversion + empty on DB failure, recordOutcome success + failure, listChangesAwaitingOutcome + aggregateUserSavings sum + dailySavingsForUser bucket + ascending order. /api/cron/run-optimizer gains 11 route-test paths: auth tri-state + zero-accounts + null externalAccountId skip + treatment-arm persist + control-arm early skip + hold/insufficient don't pollute table + per-account error isolation + meta carrier. /api/cron/measure-outcomes gains 10 route-test paths: auth + OUTCOME_WINDOW_DAYS env + normal outcome + null window → zero-spend outcome + orphan ad_account skip + per-change fetcher error isolation
- v3 change-point detection (fixes the biggest math gap from v0.23). posterior.detectDownwardShift compares spend-weighted ROAS of recent 3 windows vs older windows; flags shifted when ratio < 0.85 and combined conversions ≥ 30. When shift fires AND posterior.mean < target, decision policy hard-flips to decrease_budget at -maxDeltaPercent (defends against the "max expected revenue" objective recommending +20% on a campaign already underperforming its target). Backtest slow-decline directional hit rate **33% → 69%** (the biggest v0.23 gap closed) at the cost of seasonal 56% → 36% (sinusoidal trough triggers decrease right before rebound; recentN=3 is the chosen balance, with v4 periodicity detection planned to distinguish "trend vs cycle"). 12 new test paths cover detectDownwardShift. Honest record in backtest-v0.24.md: total savings $174K → $146K (-16%) is the trade — fewer lucky upside grabs in exchange for fewer wrong-direction calls on dangerous declines
- v4 periodicity detection (fixes the v3 seasonal collateral). posterior.detectPeriodicity computes lag-4 Pearson autocorrelation on the DETRENDED ROAS series (lag 4 = monthly cycle assuming weekly windows, most common ad-platform seasonality). When correlation ≥ 0.5, decision pipeline suppresses change-point shift — "recent decline" was actually a known-cycle trough that will rebound. Critical design: detrend (subtract linear OLS fit) BEFORE autocorrelation; otherwise a slow monotonic decline false-positives as periodic (both halves are positively correlated when both are trending down). Backtest seasonal hit rate 36% → **47%** (+11 pp), seasonal savings $10K → **$19K** (+89%); slow-decline stays at 69% (no periodicity, new path doesn't affect it). 10 new test paths cover detectPeriodicity: clear sinusoid, noisy sinusoid, monotonic-trend-no-false-positive, flat, white noise, threshold + minWindows. Backtest report v0.24.md updated to reflect the v4 path
- MCP server delivers on the long-standing /vs/adwhiz promise. New mcp-server/ sub-package, zero runtime deps (pure Node 18+ ESM + fetch + hand-rolled JSON-RPC 2.0 over stdin/stdout), lets Claude Desktop / Cursor read AdWhiz state directly. 5 read-only tools: list_ad_accounts / list_recommendations / get_savings_summary / get_recent_activity / get_quota — all routed through /api/v1 + Bearer auth. User install: git clone + add path to claude_desktop_config.json + set ADWHIZ_API_KEY env. **Deliberately no mutation tools** (apply / pause) — those wait until the optimizer exits shadow mode. Full README.md covers Claude Desktop + Cursor + generic MCP client install paths + troubleshooting + security model. 4 stdio-protocol smoke tests (spawn subprocess, exercise real protocol): initialize / tools/list / ping / unknown method
- Production path ready: when ANY of Google Ads dev token / Meta App Review / TikTok Business API approval lands → implement that platform's fetcher → set USE_REAL_ADS_FETCHER=1 → wire EventBridge to call both cron endpoints → savings hero shows its first real "本月已为你省下 $XXX" number
- v0.23CoreUX
Prediction engine v2: time-weighted posterior + probabilistic confidence + response curve
The v0.22 backtest exposed three real issues: (1) slow-decline scenario hit rate 33% (worse than coin flip) (2) confidence was binary-by-construction (3) calibration was systematically overconfident. v0.23 fixes all three. New aggregateRecency exponentially decays older snapshots into one effective snapshot (default half-life ~2.3 windows), decision is rewritten to compute P(new_revenue > current_revenue) probabilistically, elasticity is sampled from Beta(2,5) × 2 (5-15% chance > 1 to model saturated markets). The 6-scenario backtest now shows ~2× total savings vs v0.22 (~$80K → ~$174K), steady loser flipped from -$2.7K to +$208, and calibration is perfect on healthy scenarios (85% bucket → 85-87% actual positive rate).
- posterior.ts: aggregateRecency(snapshots, decay) collapses N snapshots into one effective snapshot with exp(-decay × age) weights. Default decay 0.3, half-life ~2.3 windows. Beta α/β accept fractional sums (conjugate prior math doesn't require integer counts). 12 unit-test paths cover fast path, decay=0, varying decay strengths, cold-start, zero-windows
- decision.ts rewrite: (1) defaults to aggregateRecency over the full history instead of the latest single snapshot — directly fixes the slow-decline "trust the old data" bug (2) confidence formula changes from 1 - 2|δ| to P(new_revenue > current_revenue) using paired posterior + elasticity samples (3) adds budget→ROAS response curve: new_roas = roas × (current_budget / new_budget)^elasticity where elasticity sampled from Beta(2,5) × 2 (support [0, 2], 5-15% chance > 1 to model saturated markets where bigger budget loses money)
- backtest framework: inclusive upper bound on the highest confidence bucket (≤ 1.0) so confidence=1.0 lands in [0.9, 1.0] instead of being dropped. Version-suffixed output path (--out=docs/backtest-vXX.md) for version-over-version diffing in the repo
- Backtest data: 6 scenarios × 20 campaigns × 9 weeks (1,080 total decisions). Steady winner +$56K (v0.22: +$27K), Regime change +$61K (+$29K), Slow decline +$20K (+$7.7K), Seasonal +$27K (+$12K), Sparse +$10K (+$5.5K), Steady loser **+$208 (flipped from -$2.7K)**. Calibration: winner / regime-change 85% bucket → 85-87% actual positive rate (perfect), volatile scenarios (decline / seasonal / sparse) still Δ=-13 to -21% overconfident — v3 will add regime-stability factor
- sampleBeta exported from posterior.ts so decision.ts can share the sampler (previously private). v1 stale "minConfidence=0.99 forces hold" test rewritten as "no candidate clears 1.01 threshold" + new assertion that confidence is a real probability in [0, 1]
- Honest production path: v0.23 is safe for shadow-mode on ANY campaign type (even slow-decline nets +$20K because safety constraints hold). Conservative auto-apply is viable for stable/growing campaigns; declining-campaign auto-apply should wait for v3 change-point detection
- v0.22CorePlatform
Product core: prediction engine + counterfactual savings measurement
Cut the "add more features" reflex and refocused engineering on one thing: making the AI calculate "should this ad get more or less budget" accurately enough that we can honestly tell the user "you saved $X,XXX this month". Added lib/budget-optimizer (Bayesian posterior + decision policy + hard safety caps) and lib/savings-tracker (counterfactual measurement + 15% holdout control arm). Dashboard hero is now the savings number + daily detail with negative days shown verbatim. LLMs are translators, not predictors.
- lib/budget-optimizer/posterior.ts — Beta-binomial conjugate prior (default Jeffreys Beta(0.5, 0.5)) + Marsaglia–Tsang gamma sampler + deterministic mulberry32 PRNG (so audit logs are replayable) → posterior mean ROAS + 10/90 percentiles + relativeWidth
- lib/budget-optimizer/decision.ts — 5-stage pipeline: (1) data-quality gate (conv≥5, relativeWidth≤1) (2) pause gate when posterior p90 < target × 0.5 (3) argmax expected revenue over δ ∈ {-20%, -10%, 0, +10%, +20%} candidates (4) linearity-penalty confidence = 1 - 2|δ| capped at min 0.70 (5) hold when lift < 1% of current to avoid pointless jitter
- lib/savings-tracker/counterfactual.ts — counterfactual revenue uses the PREDICTED ROAS at decision time, not the actual post-hoc ROAS (which would be circular). netSavings = profit_actual - profit_counterfactual. aggregateSavings + rollupDaily roll up for the dashboard chart
- lib/savings-tracker/holdout.ts — SHA-256(salt, userId, campaignId) deterministic bucketing into control / treatment, 15% control by default. Control arm is never touched by AI, giving us an empirical baseline. Salt bumps re-randomise for new model versions
- SavingsHero component — dashboard top huge number "Saved this month $X,XXX" + last-7-day + lifetime + change count + 30-day daily bar chart (negative days rendered in rose, no smoothing). When zero AI changes, renders honest empty state instead of fake "$0 saved"
- 58 new math tests (17 posterior + 17 decision + 13 counterfactual + 11 holdout) cover Bayesian posterior + decision policy + counterfactual measurement + holdout bucketing. Total tests 550→608, 0 lint warnings, build all green
- docs/budget-optimizer-design.md — full design doc: math, safety constraints, counterfactual measurement, shadow→conservative→production validation plan. Single source of truth for future engineers, beta users, and platform reviewers
- v0.21SecurityNewUX
Email verification flow + Dashboard demo-data notice
Closed the signup loop with proper email verification: new table, send-on-register, dedicated landing page, /settings banner with one-click resend. Dashboard KPI strip now wears an honest "demo numbers" banner when no real ad_account is connected — no more new users thinking the sample spend is theirs.
- New email_verification_tokens table (mirrors password_reset_tokens) + auto-send at register (24h expiry)
- /api/auth/email-verify/confirm endpoint: rate-limited, idempotent (alreadyVerified flag), TOCTOU-safe
- /[lang]/verify-email landing + VerifyEmailRunner posts on mount and renders all four outcomes
- /api/auth/email-verify/request resend endpoint: per-user rate-limit, no-op for already-verified
- Settings top banner shows when emailVerified is null with one-click resend (self-hides after confirm)
- Dashboard renders an amber "These are demo numbers" banner + /connect CTA when no live ad_account exists
- /api/account/export GDPR data-portability endpoint: one-shot JSON dump (profile + subs + ad_accounts + api-keys metadata + last-90-day audit_events). Secrets always excluded
- /api/v1/audit-events SDK endpoint: API-key-authed, returns the caller's audit history with ?since + ?limit (max 200)
- instrumentation.ts: inits Sentry + installs globalThis.__sentry when SENTRY_DSN + @sentry/nextjs are both present; otherwise silent no-op
- /legal/privacy Your Rights rewrite: each right now points to its self-serve path (Settings / /api/account/export) — users don't need to email support to exercise them
- Content-Security-Policy-Report-Only header live (default-src self, frame-ancestors none, upgrade-insecure-requests, etc) — observation period before flipping to enforcing mode
- /api/csp-report sink: accepts both legacy report-uri and modern Reporting-API formats, logs each violation via logger.warn, per-IP rate-limited at 200/min
- /api/v1/usage endpoint: SDKs read their tier + RPM limit + remaining + reset_at without a probe request (same data as X-RateLimit-* headers)
- README refreshed: 80+ pages, 25+ APIs, 11 tables, 135 tests, 13 migrations; enumerates the public API endpoints + Sentry / CSP wire-up notes
- Stripe webhook diffs the pre-upsert row: plan change → subscription_changed event, status→canceled → subscription_canceled. Idempotent across Stripe retries
- RecentActivity renders subscription events with the from→to transition; /api/auth/email-verify/request gains 6-path test coverage (unauth / rate-limit / already-verified / unverified send / orphan)
- Auth.js signIn / signOut events now capture x-forwarded-for + UA via next/headers, populating audit_events ip/ua; /settings#security renders "Signed in · provider · IP X.X.X.X"
- /api/v1/me gains 5 test paths (unauth / orphaned key / success envelope / null last_login / count fallback)
- Token-refresh cron writes ad_account_token_expired audit on the connected → token_expired transition; users see "Reconnect X" in /settings#security
- UnverifiedEmailBanner now also lives at the top of /dashboard — OAuth users with provider-verified email never see it
- Vitest suite up to 140 tests (/api/v1/me + /api/auth/email-verify/request)
- /api/v1/audit-events gains ?kind= filter (comma-separated, returns 400 invalid_kind on typo) — SIEM consumers can subscribe to just sign_in / api_key_* etc
- /docs/security-whitepaper upgraded from keyPoints outline to full body article (encryption, audit trail, self-serve rights, CSP, observability, AI data policy)
- /api/v1/me gains an email_verified boolean — SDK consumers can gate unverified-user UX
- /api/v1/api-keys lists the caller's full key set (lastUsedAt + active boolean), hashedKey never returned
- /admin "new users" KPI gains week-over-week +N (+X%) badge so operators see acceleration at a glance
- CONTRIBUTING documents the audit-events checklist, the vi.hoisted test pattern, and the optional Sentry wire-up
- Vitest suite up to 149 tests
- /api-reference public page synced to 13 endpoints (adds /audit-events / /api-keys / /usage / /changelog)
- lib/api-auth.ts validateApiKey gains 7 tests sealing the /api/v1 auth path (missing header, malformed, unknown hash, match, lastUsedAt fire-and-forget, test vs live env)
- lib/api-response.ts gains 10 tests covering every branch of apiOk / apiError / preflightResponse / authError
- Vitest suite final count: 166 tests across 27 files
- observability.ts + refresh-tokens cron TODOs resolved; per-row failures now emit logger.warn for CloudWatch
- Settings server actions: 12 tests across updateProfile / updatePreferredLocale / updateAiProvider / updateEmailPreferences / dismissOnboarding
- Vitest suite up to 178 tests across 28 files; README header stats refreshed
- /api/contact gains 7 test paths including honeypot drop + DB error 500
- /api/auth/register gains 8 test paths (dual-insert ordering, Accept-Language locale selection, verify-token failure still 201)
- Vitest suite final final count: 193 tests across 30 files
- settings actions: full destructive path coverage — changePassword (6) + deleteAccount (5) + disconnectAdAccount (3), including bcrypt mocks + audit-event assertions
- Vitest suite up to 207 tests
- Homepage + /vs/adwhiz inject schema.org JSON-LD (SoftwareApplication / Product with Offer pricing); rich snippets light up on Google
- .github/workflows/ci.yml runs lint+test+build on every PR (with concurrency cancel-in-progress); decoupled from deploy.yml
- /api/cron/prune-tokens weekly cleans password_reset_tokens (>7d) and email_verification_tokens (>30d) leftovers; 5 test paths
- infra/README cron table + schedule-expression cheatsheet now includes the 2 new prune crons
- /api/auth/email-verify/confirm gains the 429 rate-limit test (asserts the DB short-circuits — never queried)
- Vitest suite up to 213 tests
- Vitest include broadened to .test.{ts,tsx}; JsonLd component gets 4 tests covering </script> escape defense + nested objects
- /api/v1/openapi.json: OpenAPI 3.1 spec for the 5 stable endpoints + every schema. SDK authors can run `openapi-typescript` / `openapi-generator` directly against it
- lib/audit-events centralizes AUDIT_EVENT_KINDS + ALLOWED_KINDS — API route validation, TS union, OpenAPI enum all share one source
- CONTRIBUTING adds an "Adding a public API endpoint" checklist; README mentions the OpenAPI spec URL; /api-reference gains an OpenAPI button
- Vitest suite up to 223 tests (OpenAPI route: 6 paths covering shape / endpoint list / security override / ETag / kind sync / cache headers)
- New-device sign-in email alert: UA-hash fingerprint stored in audit_events.meta.device_fp; signIn event checks 30-day window and fires email when fingerprint is new AND user has prior sign_ins (suppresses on first-ever login to avoid welcome-email spam)
- /api/auth/password-reset/{request,confirm} route tests: 14 paths covering the "always 204" anti-enumeration contract + DB-error-doesn't-leak + double-update success
- Vitest suite final count: 237 tests across 35 files
- /about, /legal/privacy, /legal/terms, and the AI system prompt now correctly say "Google Ads, Meta Ads, and TikTok Ads" — the last few 2-platform stragglers
- /api/oauth/google/start route tests: 5 paths verifying CSRF state cookie (HttpOnly + SameSite=lax) + fresh-per-request token
- package.json bumped to 0.21.0
- /api/cron/trial-reminders tests (7 paths covering plan-label + locale mapping + daysLeft floor); /api/cron/monthly-digest tests (6 paths covering cross-account dedupe + locale fallback + null-email skip)
- Vitest suite final final final count: 255 tests across 38 files; nearly every active cron endpoint now has coverage
- instrumentation.ts hides the @sentry/nextjs import behind a runtime-computed module id so the bundler doesn't force install at build — `npm install @sentry/nextjs` is all an operator needs
- Vitest suite up to 120 tests (/api/v1/audit-events 6 paths + /api/account/export 5 paths — verifies attachment header, ISO serialization, and that passwordHash / hashedKey / encrypted tokens never appear in the dump)
- /api/v1/changelog gains ?limit=N (1..50) + ?since=YYYY-MM-DD: limit returns the newest N, since filters by entry date — together they cover the canonical incremental-poll pattern. ETag now fingerprints (lang, limit, since, content) so consumers never get a false 304 across variants. OpenAPI + tests updated to match
- /api/v1/accounts gains 6 route-test paths: unauth pass-through, success envelope, spend_usd 2-decimal precision, environment echo, X-RateLimit-* header injection, OPTIONS preflight
- /api/v1/recommendations gains 8 route-test paths covering unauth, default status=pending, all-supersets-pending, strict applied filter, unknown-status-returns-empty (not 400), SDK shape, rate-limit header, OPTIONS
- /api/v1/audit-log marked deprecated — RFC 8594 Deprecation: true + Sunset (2027-05-23) + Link rel=successor-version steering to /audit-events. Non-breaking migration signal; 7 route-test paths added
- /api/v1/accounts/[id]/campaigns gains 6 route-test paths: unauth, unknown-id 404 + account_not_found code, full success envelope, spend_usd + cpa_usd precision, rate-limit header, OPTIONS
- /api/v1/recommendations/[id]/apply + /dismiss gain 11 route-test paths together: apply covers unauth / applied_at fresh / rate-limit header; dismiss covers empty body / text reason / >500-char truncation / non-string-coerces-to-null / malformed-JSON tolerance / OPTIONS
- /api/v1/changes/[id]/rollback gains 5 route-test paths: unauth, id-echo + reverted_at within request window, audit-id uniqueness across calls, rate-limit header, OPTIONS
- /api/v1/changelog gains Last-Modified + If-Modified-Since 304s — curl -z / wget --time-stamping clients can now do date-based conditional GET. Sends both ETag and Last-Modified; ETag wins when both client headers are present (RFC 9110 §13.1). OpenAPI updated; 5 route-test paths cover the new branches
- /changelog.rss now emits Last-Modified anchored to the newest entry — RSS aggregators (Feedly, NewsBlur) use If-Modified-Since to save bandwidth. The route stays force-static; we just advertise the timestamp
- New "Incrementally polling /changelog" section on /api-reference with 4 curl snippets covering ETag / Last-Modified / ?since / ?limit — SDK authors see the recommended pattern at a glance
- /api/oauth/meta/start + /api/oauth/tiktok/start gain 5 route-test paths each, mirroring google/start: unauth redirect, lang default, not_configured, state-cookie CSRF flags, per-request fresh state
- /api/cron/refresh-tokens gains 9 route-test paths: CRON_SECRET-unset 503, wrong-bearer 401, missing-header 401, crypto-unconfigured 503, zero-candidates 200, all-three-platforms dispatch + refreshed, refresh-failure writes token_expired + audit event, unknown-platform failed, GET-method compatibility
- /api/ai/recommend gains 6 route-test paths: no-session 401, session-without-id 401, providerOverride from users.aiProvider, missing-user-row falls back to env, AI-throws-Error 500 + captureError context, non-Error-throw degrades to ai_failed
- /api/stripe/webhook gains 7 route-test paths covering all security-critical gates: missing-signature 400, missing-secret 400, SDK-throws-on-bad-sig 400 with message echo, unique-violation dedup returns duplicate:true without Sentry spam, non-dedup DB error 500 + captureError(stage=dedup_insert), handler failure rolls back the dedup row + 500 + captureError(stage=handler), happy path 200 received
- /api/oauth/google/callback gains 11 route-test paths: unauth, state-mismatch (+ no-cookie sub-branch) burns cookie, user-decline error echo, missing-code, not-configured, crypto-unconfigured, token-exchange-failure + captureError ctx, no-refresh-token (skips insert), persist-failure suppresses audit, happy-path redirect + audit + cookie clear
- /api/oauth/meta/callback gains 7 route-test paths: unauth, state mismatch + cookie burn, user-decline echo, step-1 short-token exchange failure, step-2 long-lived exchange failure, long-lived token written to BOTH access + refresh columns (Meta has no separate refresh), persist-failure suppresses audit
- /api/oauth/tiktok/callback gains 8 route-test paths: unauth, state mismatch + cookie burn, code-fallback (TikTok natively uses auth_code), exchange failure, N advertiser_ids → N rows + N audit events, empty advertiser_ids inserts a single null-id row, access + refresh encrypted independently (TikTok rotates both), persist-failure suppresses audit
- /api/v1/recommendations/generate gains 7 route-test paths: unauth pass-through, body echo + SDK envelope, recommendation shape, job_id uniqueness (5 calls ≥ 2 distinct), malformed-JSON echo={}, rate-limit header, OPTIONS preflight
- lib/email gains 10 unit-test paths: sendEmail covers no_api_key skip + Resend OK + rejected-message-without-Sentry-spam + thrown-error captureError ctx; sendContactNotification routes to SUPPORT_INBOX with replyTo + handles null message; sendPasswordReset URL-encodes token + zh-CN copy switch; sendNewDeviceAlert truncates UA at 80 chars + null IP renders em-dash placeholder
- OpenAPI info.version bumped 1.0.0 → 1.1.0 to reflect the additive surface accumulated since 1.0: /changelog gains ?limit + ?since + Last-Modified + If-Modified-Since, /audit-log gains RFC 8594 Deprecation headers. No breaking changes; SDKs built against 1.0 stay compatible
- /vs/adwhiz: stale "8 core endpoints" copy synced to 13 (+ OpenAPI 3.1 + ETag/304 incremental polling). /api-reference /audit-log row now flags Deprecated + sunset 2027-05-23 + steers to /audit-events
- src/proxy.ts (Next.js 16 middleware) gains 14 test paths: i18n redirect (default / zh / en / path-segment preservation / locale-already-present skip / unknown-locale falls back), /api skip, request-id upstream precedence (x-request-id → x-amzn-trace-id ALB → cf-ray Cloudflare → fresh UUIDv4), matcher excludes _next/static and other hot paths
- lib/mock-ads-data gains 8 unit-test paths: MOCK_ACCOUNTS fixture invariants (all three platforms, unique account ids, unique per-account campaign ids, non-negative metrics + conversions ≤ clicks), summarizeAccount sums correctly + counts only active + zero-on-empty + leaves rounding to the route layer
- /changelog gains a client-side "N new since you last visited" pill — localStorage-backed (dxk_changelog_last_visit), dismissible, silently seeds on first visit (no banner flash). Pure countNewSince helper extracted + tested with 7 paths (today / N=2 / all-new / future date / strict > boundary / empty / lexical YYYY-MM-DD ordering across year+month rollover)
- lib/docs-articles + lib/blog-posts gain 18 unit-test paths: ARTICLES/POSTS fixture invariants (unique slugs, known CategoryKey, no orphans, bilingual body, ISO publishedAt, localised date strings) + articleHref (redirect → absolute, default → /docs/[slug], lang-aware) + relatedArticles/Posts excluding seed + respecting limit
- Extracted fingerprintUA from auth.ts into lib/ua-fingerprint with 8 unit-test paths (null / undefined / empty all return null; non-empty UA → 16-char lowercase hex; deterministic on same input; sensitive enough to differentiate Chrome 120 vs 121; handles ~500-char pathological UAs). New-device login alert is unchanged — the refactor only makes the algorithm independently testable + reusable
- Security headers gain Cross-Origin-Opener-Policy: same-origin-allow-popups (hardens against tabnabbing / opener-leakage while keeping OAuth popups working) + Cross-Origin-Resource-Policy: same-site (allows app.7275.com ↔ 7275.com under one registrable domain, blocks bare cross-site embeds). Low-risk modern defaults; existing OAuth / Stripe flows untouched
- lib/audit-events gains 4 taxonomy test paths: tuple shape preserved / all 10 stable slugs present / enforced snake_case (renaming would break historical audit rows) / ALLOWED_KINDS Set strictly rejects typos + empty + uppercase. The kind enum is the single source of truth read by route validation, OpenAPI spec, and the settings UI
- Extracted eventLabel + summaryLine helpers from RecentActivity component + 14 unit-test paths: eventLabel covers all 10 slugs + unknown-slug graceful fallback; summaryLine covers sign_in/out tri-state (provider+IP / IP-only / em-dash / non-string-provider safety), api_key name·env concat, ad_account platform-suffix trim, subscription_changed both shapes (plan_changed / created), canceled, generic meta-null with IP fallback, and double-null em-dash
- CrossPlatformPanel aggregatePlatform + platform helpers exported with 11 unit-test paths: arithmetic sums; ROAS is spend-weighted (not arithmetic mean — defends against tiny-budget-high-ROAS outliers); spend=0 yields ROAS 0 not NaN; conv=0 yields CPA 0 not Infinity; only active campaigns counted; empty-array safe; platformLabel/Letter/Accent map all three + Tailwind from-/to- class strings stay intact
- AuditLogTable filterEntries + Filter type exported + 8 unit-test paths: all returns full list, ai-only / user-only (excludes system), three status filters, source-order-preserving filter (no internal sort), empty array under any filter returns []
- lib/ai/schema gains 10 invariant test paths: RECOMMENDATION_TYPES preserves 7-verb stable order + snake_case + uniqueness; JSON schema top-level requires both summary + recommendations; the recommendations array is capped 1..8 (defends against AI runaway); the enum mirrors the tuple exactly (drift would silently drop AI completions); all 7 per-rec fields required; confidence enum closed to low/medium/high; target requires only accountId; additionalProperties:false at every nesting level (closes the hallucinated-field loophole)
- lib/ai/index activeProvider exported with 6 unit-test paths pinning the resolution precedence: per-user override > AI_PROVIDER env > hardcoded "anthropic" fallback; null/undefined/empty all pass through to env; case + whitespace are normalised (" OpenAI " → openai); unknown values at either layer fall through cleanly (cohere → env, then palm → hardcoded)
- lib/ai/prompt gains 7 unit-test paths: SYSTEM_PROMPT contains no ${} template strings or ISO dates (preserves Anthropic prompt-cache hits across requests) + all three platform names present + tool/JSON output directive enforced; buildUserPrompt embeds the accounts snapshot inside a ```json fence that JSON.parses back to the input, handles multi-account + empty input + closes with an imperative ask that pushes the model toward concrete recommendations
- lib/ai/anthropic + lib/ai/openai gain 5 + 6 = 11 adapter test paths: missing-API-key clean throw, tool_use / json_schema parse + usage forwarding, missing-tool-block or null-content throws (defends against corrupt empty recommendations landing in the UI), wrong tool name ignored, SDK client cached (one ctor across many calls), openai also covers JSON.parse propagation should strict-mode ever loosen
- /settings/api-keys actions gain 14 test paths: createApiKey covers unauth, empty name, whitespace-only name, 81-char name (>cap), live + test env prefix, missing/invalid env defaults to live, success path produces 41-char plaintext + 13-char prefix + writes api_key_created audit, DB failure returns db_error AND skips audit (no ghost rows in /settings#security), each call yields a fresh plaintext; revokeApiKey covers unauth, missing id, successful revoke writes api_key_revoked audit, already-revoked does not double-audit
- OpenAPI spec gains 5 semantic tag groups (Identity / Quota / Activity / Keys / Public) — Swagger UI / ReDoc / Postman auto-fold endpoints under these section headers. Every path is tagged (no orphans render as a phantom "default" group). Purely additive; existing SDK consumers untouched. 2 new test paths assert tags-declared + descriptions present + every operation tagged with a declared group
- lib/google-oauth gains 8 HTTP-path tests for exchangeCode + refreshAccessToken: 200 parses access/refresh/expires_in; form-urlencoded body carries all 5 required Google fields; 4xx throws with status + body-truncated-to-240; 5xx also throws; network errors propagate unwrapped; refresh-flow sends grant_type=refresh_token (distinct from authorization_code, and correctly omits redirect_uri); refresh 401 (expired token) message is operator-actionable
- lib/meta-oauth + lib/tiktok-oauth gain 15 HTTP-path tests: Meta's two-step short → long-lived flow uses GET + query-string (Meta quirk vs POST + body), long-lived swap uses grant_type=fb_exchange_token and omits `code`, 4xx throws with status; TikTok uses POST JSON envelope (`auth_code` field NOT `code`, HTTP 200 + non-zero business code throws), refresh hits a distinct endpoint from access_token, and both tokens rotate together on refresh (a foot-gun the test pins down)
- v0.20SecurityPlatformNew
Audit + open-API batch: audit_events table, security headers, public API, anti-spam
Stood up a full security-event audit trail (sign-in, password change, API key issuance, OAuth grants) — users see their own activity on /settings, operators see the whole org on /admin. Plus baseline security headers, a contact-form honeypot, and a public /api/v1/changelog JSON endpoint.
- New audit_events table (kind / userId / ip / ua / JSONB meta) with writes from 6 event types
- /settings Security shows your last 10 events; /admin gets a global last-12 panel
- /api/cron/prune-audit-events sweeps rows older than 180 days weekly (override via AUDIT_RETENTION_DAYS)
- Baseline security headers across all routes: X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy, Permissions-Policy
- Contact form honeypot: silently drops bot submissions (no DB write, no email) while returning the normal success envelope
- Public /api/v1/changelog JSON endpoint (lang switch, CDN-friendly caching) sharing data with /changelog.rss
- API key lastUsedAt now rendered as relative time on /settings/api-keys; auth middleware stamps it on every authenticated request
- Auth.js signOut event also writes to audit_events — full session-lifecycle audit loop
- Sentry shim: lib/observability forwards via globalThis.__sentry when SENTRY_DSN is set — no hard dep on @sentry/nextjs
- relativeTime promoted to src/lib/relative-time.ts (5 surfaces share it) with years-unit coverage
- Footer copyright year auto-rolls (template token + new Date().getUTCFullYear()); package.json bumped to 0.20.0
- /audit-log header note clarifies it covers AI campaign changes; sign-ins / API keys / etc live on /settings#security
- /api/v1/me now returns last_login_at + connected_accounts_count so SDK consumers can render rich account state
- /api/v1/changelog gains ETag + If-None-Match 304 — long-polling consumers stop redownloading unchanged content
- audit_events gains a (userId, createdAt DESC) composite index — /settings query drops from seq-scan to index-only
- /admin Recent users gains a Plan column (free muted, professional indigo, business violet chip)
- docs/observability.md documents the full Sentry wire-up (instrumentation.ts template + rationale for skipping withSentryConfig)
- Vitest suite up to 103 tests (ETag + 304, Sentry shim 4 branches, prune-audit-events cron 4 paths)
- v0.19UXPlatform
Preferences-persistence batch: email opt-ins, onboarding sync, login history, last incident
Pulled the dashboard / settings state that previously lived only in localStorage down into the database so it survives device switches; added an MRR-estimate KPI and "last seen" column to admin; status page now pulls incidents from a real data module instead of hardcoded literals.
- New users.emailWeeklyDigest / emailRecAlerts columns + EmailPreferences component; monthly-digest cron now filters opt-outs at the query level
- Onboarding "dismissed" now persisted to users.onboardingDismissedAt — same state across devices; prefilledDone hints now derive from real ad_accounts / api_keys rows
- Auth.js signIn event stamps users.lastLoginAt; Settings shows "Last sign-in X days ago", Admin gets a Last seen column
- Settings profile section now shows "Member since YYYY-MM-DD (X months ago)"
- Status-page incidents moved to src/data/incidents.ts module; header gains a "Last incident: X days ago" pill
- Admin KPI grid gains an MRR-estimate card (active subs × plan monthly price, yearly subs at 80% monthly-equivalent)
- Changelog data extracted to src/data/changelog.ts; new public /changelog.rss feed
- Vitest suite up to 82 tests (incidents data helpers + mrrContribution + RSS route smoke)
- v0.18NewSecurityPlatform
Production-ready batch: 3-platform OAuth, password reset, Stripe idempotency, observability
Shipped everything needed before a real launch: end-to-end OAuth lifecycle for Google / Meta / TikTok (connect → refresh → disconnect), forgot-password flow, Stripe webhook idempotency dedup, AES-256-GCM token encryption at rest, structured logging + error-capture hook, CI test baseline (63 unit tests), and operator-side connected-accounts admin panel.
- Real OAuth flows for all three platforms with AES-256-GCM token encryption at rest
- EventBridge cron endpoints: token refresh every 30 min, daily T-3 trial reminders, monthly performance digest
- Stripe webhook idempotency via stripe_events dedup table; added invoice.payment_failed / trial_will_end / payment_succeeded handlers
- Forgot-password end-to-end: emailed token, 15-min expiry, rate-limited, no account-existence oracle
- Observability: lib/logger (structured JSON) + captureError() error hook + X-Request-Id correlation header
- Vitest with 63 unit tests covering crypto, rate-limit, OAuth, API key validation, admin allowlist, stripe helpers, health endpoint
- Settings now shows connected accounts with one-click disconnect; Admin gets a recent-connections table + expired-token KPI alert
- New users.preferredLocale column — cron emails respect per-user language, no more hardcoded zh-CN
- v0.16NewUX
Light/Dark theme + dedicated "DEXUN AdWhiz vs AdWhiz.ai" comparison page
Full-site light/dark theme support (system-follow or manual toggle), plus a dedicated comparison page laying out how DEXUN AdWhiz and AdWhiz.ai differ across platforms, MCP, REST API, annual billing, and affiliate.
- All pages (home, docs, blog, dashboard, legal) support both themes — eye-friendly for late-night ops
- Sun/moon toggle in the top-right; first visit defaults to system preference
- New /vs/adwhiz page: 12-row feature checkmark grid, 4 deep-dive topics, 3-tier pricing comparison
- v0.15New
Annual vs monthly billing toggle + 20% off on annual
Added monthly/yearly segmented toggle to Pricing. Annual saves 20% (pay for 10, get 12). Stripe backend routes to STRIPE_PRICE_*_YEARLY price IDs automatically.
- Pro: $63.20/mo annual (was $79) · Business: $223.20/mo (was $279) · Enterprise: $639.20/mo (was $799)
- "Save 20%" badge uses indigo→violet gradient matching the footer affiliate badge
- v0.14NewUX
Homepage trust section + tiered audit frequency in Pricing
New trust section between Features and Pricing showing only checkable facts (control-group comparison, ROI money-back guarantee, publicly disclosed operator DEXUN INC). Pricing now describes per-tier audit cadence: daily / 4-hour / hourly.
- Trust section: control-group comparison · ROI guarantee · operated by DEXUN INC (no fabricated customers / numbers / reviews)
- Pricing: Pro daily (20/day) · Business 4-hour (100/day) · Enterprise hourly (high quota)
- v0.13PlatformNew
TikTok Ads support — we're now three-platform
TikTok Ads is the third supported ad platform. Mock data, i18n strings, Pricing copy, FAQ, and Hero bento all updated.
- Dashboard: Google / Meta / TikTok account cards side-by-side, KPIs auto-aggregated
- AI recommendations now suggest cross-platform shifts (e.g. "move Meta lookalike to TikTok Spark")
- v0.12New
Affiliate program launched (its original tiers were retired in 2026-10)
/affiliates is live: 60-day cookie, lifetime recurring (not first-payment-only). Footer adds an affiliate badge in the Company column. The original four-tier rates are retired; current rates are 10% standard and up to 20% for selected partners, as on the affiliate page.
- (Retired) four commission tiers promoted by active referral count
- Net-30 payouts, PayPal or Stripe transfer, $50 minimum
- v0.11NewUX
Docs / Blog / API reference / MCP guide all shipped
/docs and /blog ship as dynamic [slug] routes: 7 full articles + 21 structured previews + 6 blog posts. /api-reference enumerates 8 REST endpoints with error codes. /mcp-guide explains Claude / Cursor integration in detail.
- Shared markdown-lite renderer (**bold** + [text](url)) across Docs and Blog
- Docs has popular-shortcuts row; Blog has a featured-post card and topic tag cloud
- v0.10UXNew
Hero redesigned (left text / right bento) + Apple-style segmented control
Homepage Hero rebuilt as left-text / right-bento, with Google / Meta / TikTok mini cards on the right. Nav swap to Apple-style segmented control for tab switching.
- Three platform mini-cards rotate real account names + status + 7-day ROAS
- Primary CTA changed from "Get started" to a 7-day free-trial message
- v0.9NewUX
About / Contact / Coming-soon support pages + contact form API
/about (parent company, registration, business info), /contact (form + DB persistence + email notify), /coming-soon (shared placeholder taking ?topic= param) all live. Every Footer link now routes somewhere meaningful.
- contact_submissions table + /api/contact endpoint + Zod validation
- About discloses legal entity DEXUN INC, Seattle WA registration, parent DEAO GROUP LIMITED
- v0.8NewSecurity
Four legal pages live (Privacy / Terms / Refund / Cookies)
Privacy, Terms, Refund, and Cookies legal pages all published. Governing law moved to Washington State + AAA arbitration + King County. Contact email unified across pages: [email protected].
- Shared LegalLayout: sticky left ToC, prose right, dark-mode aware
- v0.7NewPlatform
Stripe subscriptions + Customer Portal + Webhook integration
/[lang]/checkout server-side redirect to Stripe Checkout. /api/stripe/webhook handles customer.subscription.* events. Dashboard shows subscription status + Customer Portal entry point.
- subscriptions table + users.stripeCustomerId; status mapped to active / trialing / past_due / etc.
- 7-day free trial, no card charged during trial, auto-converts to monthly after
- v0.6NewSecurity
AI recommendations layer (Anthropic + OpenAI dual-provider)
AI abstraction layer supports runtime switching between Anthropic Claude and OpenAI (AI_PROVIDER env). Structured-output schema validation. Dashboard shows mock accounts + real AI-generated recommendations.
- /api/ai/recommend route: account snapshot in, 3-5 structured recommendations out (type / target / reason / expectedImpact)
- v0.5NewSecurity
Auth.js v5 integration (Credentials + Google + GitHub)
Drizzle adapter wired into Auth.js v5 beta. Email / Google / GitHub login. JWT sessions. Signup endpoint with bcrypt hashing + Zod validation.
- Dashboard is route-protected, unauthenticated users redirect to /login
- v0.1Launch
DEXUN AdWhiz public preview
Full-stack baseline live: Next.js 16, Tailwind v4, Drizzle, Supabase Postgres. Bilingual (ZH/EN) home, Pricing, FAQ, Footer shipped.
- Operated by DEXUN INC (Seattle, Washington, USA), parent company DEAO GROUP LIMITED
Curious what we're working on next?