2026 10 11 xp pro purchase design - xopherdeep/do-it-for-the-xp GitHub Wiki
Date: 2026-10-11 ยท Status: draft for review
Do It For The XP earns money from a family subscription called XP Pro, sold on the web (Stripe) and in the iOS and Android apps (App Store and Play, through RevenueCat). Gatekeeper, the COMPASS licensing plugin, is the single source of truth for who has Pro. The free app keeps the whole chore loop, so a family can run their house on it without paying.
Success means: a parent on any platform can buy Pro, the app unlocks within seconds, the unlock follows the family to their other devices, cancellations and refunds remove it, and nothing a family made is ever deleted when Pro lapses.
| Topic | Decision |
|---|---|
| Channels | Web (Stripe through Bazaar) and native stores (Apple and Google through RevenueCat) at launch |
| Source of truth | Gatekeeper. Stripe and RevenueCat are adapters that report into it |
| Webhooks | Every inbound event arrives through Hookshot, which authenticates it |
| Identity | A random Family ID per install, an XP-PRO- license key per family, email backup. No passwords. Real accounts wait for Family Sync |
| Price | $4.99/month, $39.99/year, 7-day free trial on both; Founder's Lifetime $79 one-time, for a limited time |
| Unit | Per family, never per kid |
| Pro-only at launch | Temple Creator; advanced scheduling; AI helpers |
| Free forever | Unlimited heroes; custom quests and custom beasts; claim and battle; GM approval; XP/GP/AP; rewards shop; the basic Gold Bank; Do This, Not That; every class, every region, built-in temples |
| Later Pro (roadmap, out of scope here) | Parent reports; a Pro cosmetic set for heroes; Gold Bank+ (interest, savings goals, allowance); Family Sync (multiplayer across devices) as the headline |
| Paid feature | Who charges for it | Our answer |
|---|---|---|
| Advanced scheduling, auto-assigning chores | S'mores Up ($4.99/mo) | Pro: advanced scheduling, AI schedule suggestions |
| Reports (daily/weekly/monthly, skipped tasks) | S'mores Up, Joon ($12.99/mo, $89.99/yr) | Later Pro: parent reports |
| Money skills (jars, allowance, investing) | S'mores Up (MoneyWise), Homey ($4.99/mo, $49.99/yr; 1 jar free), BusyKid (about $4/mo) | Basic Gold Bank free; later Pro: Gold Bank+ |
| Penalty management, reward approval | S'mores Up | Free: they are part of our core loop (Do This, Not That; HP; the approval queue) |
| Game customization and difficulty | Joon | Pro: Temple Creator. Custom quests and beasts stay free |
| Family-size limits | Homey (3 members free; reviewers complain) | None: unlimited heroes free |
| Exclusive cosmetics, monthly item sets | Habitica | Later Pro: a cosmetic set that adds, never takes away |
| Separate parent and kid devices | Joon (two apps) | Later Pro: Family Sync |
Nobody charges for the basic loop. They charge for parent power tools, money features and cosmetics, which is the line this spec draws.
โโโโโโโโโโโโโ Web (Cloudflare Pages) โโโโโโโโโโโโโโ
Go Pro screen โโโค GET doit.forthexp.com/buy/doitforthexp/{plan} โ
โ ?family_id&device_id&return_url=โฆ/#/pro/return โ
โโโโบ Bazaar โโบ Stripe Checkout โโบ Stripe webhook โโโ
โโโโโโโโโโโโโ iOS / Android โโโโโโโโโโโโโโโโโโโโโโโ โ
Go Pro screen โโโค RevenueCat SDK (appUserID = family_id) โ โ
โโโโบ App Store / Play โโบ RevenueCat webhook โโโโโโโค โ
โผ โผ
Hookshot receive endpoint (authenticates each source)
โ do_action xophz_hookshot_incoming
Gatekeeper XP Pro: Stripe adapter, RevenueCat adapter
โข grants or extends the family's license
โข expires or revokes on cancel, refund, expiry
โข emails the key when an email is known
โ
App โโโ POST /compass/v1/gatekeeper/xp-pro/claim {family_id, device_id}
โโโ POST /compass/v1/gatekeeper/xp-pro/validate {license_key, device_id}
Code ownership:
| Piece | Lives in | Why |
|---|---|---|
Plan catalog and /buy/doitforthexp/โฆ resolver |
xophz-compass-xp (new class-xophz-compass-xp-pro.php) |
Same pattern as the existing class-xophz-compass-xp-subscriptions.php resolver for /buy/xp/โฆ
|
| Licenses, adapters, REST endpoints |
xophz-compass-gatekeeper (new class-gatekeeper-xp-pro*.php) |
Gatekeeper already owns licenses and Hookshot fulfillment |
| Bearer authentication for RevenueCat |
xophz-compass-hookshot (Hookshot_Signature) |
Hookshot authenticates every inbound webhook |
| AI helpers proxy |
xophz-compass-gatekeeper (class-gatekeeper-xp-pro-ai.php), using COMPASS core's Google connector |
Pro AI must check the license, and the Gemini key must stay on the server |
| Checkout session creation |
xophz-compass-bazaar (unchanged) |
The existing dynamic resolver path already does everything needed |
| Paywall, purchase UI, entitlement cache | do-it-for-the-xp |
/buy/xp/โฆ and /buy/forthexp/โฆ already belong to the XP developer-API plans; the family product uses doitforthexp so the two never collide.
The XP plugin's existing POST /xp/v1/pro and _xp_is_pro user meta are a different thing: an admin-set flag on WordPress users of the COMPASS XP network. XP Pro for the family app never reads or writes them; family entitlement lives only in Gatekeeper's gk_xp_pro_* records.
Xophz_Compass_Xp_Pro::get_plans() returns:
| Plan | Stripe mode | Price | Interval | Trial | Store product id |
|---|---|---|---|---|---|
monthly |
subscription | 4.99 | month | 7 days | xp_pro_monthly |
yearly |
subscription | 39.99 | year | 7 days | xp_pro_yearly |
lifetime |
payment | 79.00 | โ | โ | xp_pro_lifetime |
lifetime is sold only while the option xp_pro_lifetime_on_sale is true (default true at launch).
It hooks xophz_resolve_buy_request and handles only segments[0] === 'doitforthexp':
-
segments[1]must be a plan in the catalog that is on sale, or the resolver returns aWP_Error(404). -
family_idmust be a lowercase UUID v4;device_idmust be 8โ64 characters of[A-Za-z0-9_-]. Anything else is a 400. Neither is ever echoed into HTML. - It returns
handled => true,product_name(notname: Bazaar'screate_stripe_sessionreadsproduct_name),price,mode,interval,trial_daysandmetadata = { product: 'doitforthexp', plan, family_id, device_id }. - It leaves
success_urlandcancel_urlunset so Bazaar builds them fromreturn_url, whichXophz_Compass_Security::is_allowed_redirect_urlchecks. Bazaar appendssession_id={CHECKOUT_SESSION_ID}&purchased=doitforthexp. - Bazaar already switches to the Stripe test key when
return_urlpoints at localhost, so local development uses test mode automatically.
GET /compass/v1/gatekeeper/xp-pro/plans (public) returns the catalog with on_sale flags so the web app shows the same prices the server charges. Gatekeeper reads the catalog through the filter gatekeeper_xp_pro_plans, which the XP plugin fills from get_plans(), so neither plugin calls the other's classes.
Gatekeeper_Xp_Pro_Licenses stores one record per family as its own option, gk_xp_pro_{family_id} (autoload off), plus a lookup option gk_xp_pro_key_{LICENSE_KEY} โ family_id. One option per family keeps two webhooks for different families from overwriting each other, which a single shared array (the pattern in Gatekeeper_Licenses) would risk. If the business passes a few thousand families, move this to a custom table behind the same class.
array(
'license_key' => 'XP-PRO-7KQM-3HXA-9WZD', // 3 groups of 4 from the unambiguous alphabet already used by Gatekeeper_Licenses
'family_id' => 'โฆuuidโฆ',
'plan' => 'monthly' | 'yearly' | 'lifetime',
'status' => 'active' | 'expired' | 'revoked',
'expires_at' => '2026-11-18T00:00:00Z' | null, // null = lifetime
'env' => 'production' | 'sandbox',
'email' => '[email protected]' | null,
'sources' => array(
'stripe' => array( 'customer' => 'cus_โฆ', 'subscription' => 'sub_โฆ', 'payment_intent' => 'pi_โฆ' ),
'revenuecat' => array( 'product_id' => 'xp_pro_yearly', 'store' => 'APP_STORE' ),
),
'devices' => array( 'device-id', โฆ ), // at most 10
'event_ids' => array( 'evt_โฆ', โฆ ), // last 50 processed events, for idempotency
'created_at' => โฆ, 'updated_at' => โฆ,
)Rules:
-
Entitled when
status === 'active'and (expires_atis null or now <expires_at+ 3 days). The 3 days cover store billing retries. -
A grant is idempotent. It merges the source refs and keeps the license key. If a family holds both a subscription and the lifetime plan, lifetime wins (
expires_at = null,plan = 'lifetime'); a later subscription event never shortens it. -
Revoke only what the refund bought. Refunding or disputing the lifetime payment sets
status = 'revoked'. Any other refund (a subscription charge) leaves an active lifetime alone and otherwise setsstatus = 'expired', so the next renewal turns Pro back on. An admin Restore action next to "Reset devices" reactivates a revoked record. -
Expire only from the current paid source. The record keeps
period_source(stripe:<sub id>orrevenuecat:<product id>) andlifetime_source. An expiry from any other source (an old subscription, the other store) changes nothing, andexpires_atonly ever moves later (max). - Production wins. A sandbox event never changes a production record.
- Every write to a family's record runs under a per-family MySQL lock (
GET_LOCK), and the AI and rate-limit counters use atomic increments. - Both expired and revoked records are kept, so buying again reuses the same key.
- A license never goes through
Gatekeeper_Licenses::validate(). That path accepts made-upCX-keys (a separate task is closing it), and XP Pro keys must not depend on it. - When a grant creates a new key and an email is known, it sends one
wp_mailwith the key and the restore steps.
Two Hookshot webhooks are set up in wp-admin: XP Pro ยท Stripe and XP Pro ยท RevenueCat. Their post ids are saved in the option gatekeeper_xp_pro_webhooks = { stripe: <id>, revenuecat: <id> } from the Gatekeeper admin screen.
Gatekeeper_Xp_Pro_Intake hooks xophz_hookshot_incoming and processes a payload only when both hold:
-
$webhook_idis one of the two configured ids. That id decides the adapter; the payload's shape never does. -
Hookshot_Signature::was_verified( $webhook_id )is true. Hookshot lets a webhook with no secret through unsigned; an unverified XP Pro event is logged and dropped.
The existing handle_hookshot_incoming (product === 'forthexp') is left alone; it never sees doitforthexp.
Hookshot change. Hookshot_Signature::verify() today only checks HMAC signatures (Stripe t=,v1= and raw/GitHub HMAC). RevenueCat sends a fixed Authorization header instead. Add post meta hookshot_verify_mode: hmac (default, today's behavior) or bearer. In bearer mode, the Authorization header must equal Bearer {signing secret} (compared with hash_equals); on a match the request is marked verified exactly as an HMAC match is. The mode goes in the Hookshot webhook editor beside the signature header field. The Stripe webhook keeps hmac with signature header Stripe-Signature, which the existing t=,v1= parser already understands.
Stripe adapter (Gatekeeper_Xp_Pro_Stripe). Subscribe the Stripe endpoint to these events:
| Event | Action |
|---|---|
checkout.session.completed with metadata.product === 'doitforthexp' and payment_status paid or no_payment_required (an unpaid session grants nothing) |
Grant: plan from metadata; expires_at = now + 7 days if the subscription is trialing, else the period end (subscription) or null (lifetime); record customer, subscription, payment_intent, customer_details.email; env from livemode
|
checkout.session.async_payment_succeeded |
Grant, as for a paid checkout |
checkout.session.async_payment_failed |
Revoke that purchase (rules in 3.2) |
invoice.paid for a known subscription
|
Extend expires_at to the invoice's period end |
customer.subscription.deleted for a known subscription
|
Expire |
charge.refunded (full refund) or charge.dispute.created for a known payment_intent or customer
|
Revoke |
The invoice's subscription id is read from invoice.subscription or, on newer Stripe API versions, invoice.parent.subscription_details.subscription. Renewal events don't carry the session metadata, so the adapter finds the family by the subscription or payment_intent saved at checkout, through the lookup options gk_xp_pro_sub_{id} and gk_xp_pro_pi_{id} โ family_id. Events for unknown ids are ignored.
RevenueCat adapter (Gatekeeper_Xp_Pro_RevenueCat). event.app_user_id is the family_id (rejected unless it's a UUID v4). event.id is the idempotency key. event.environment sets env.
| Event type | Action |
|---|---|
INITIAL_PURCHASE, RENEWAL, UNCANCELLATION, PRODUCT_CHANGE
|
Grant or extend; expires_at from expiration_at_ms
|
NON_RENEWING_PURCHASE for xp_pro_lifetime
|
Grant lifetime |
CANCELLATION |
No change, since auto-renew is off but the paid period runs out. Revoke only when cancel_reason is CUSTOMER_SUPPORT (a refund) |
EXPIRATION |
Expire |
BILLING_ISSUE |
No change (the 3-day grace and the store's own retry cover it) |
TRANSFER, anything else |
Log only |
Every endpoint validates family_id and device_id as in 3.1, and is rate-limited per IP with a transient (30 requests a minute; /recover 5 an hour).
| Method + path | Body | Returns |
|---|---|---|
GET /xp-pro/plans |
โ | catalog and on_sale flags |
POST /xp-pro/claim |
family_id, device_id |
200 {pro, license_key, plan, expires_at, env, family_id} when a record exists; 404 {pro:false} when nothing has arrived yet. Adds the device |
POST /xp-pro/validate |
license_key, device_id |
Same 200 shape (including family_id, so a restored device adopts it), or 404. Adds the device. pro is false when the record is expired or revoked |
POST /xp-pro/recover |
email |
Always 202. Emails every key on file for that address. Never reveals whether one exists |
POST /xp-pro/manage |
license_key |
For a Stripe-sourced license: 200 {url} to a Stripe Billing Portal session that returns to return_url. For a store-sourced one: 200 {store} so the app can open the store's subscription page |
Device cap: an 11th device gets 409 {error:'device_limit'}; support can reset devices from the Gatekeeper admin screen, which lists XP Pro licenses next to the existing ones.
The family_id works like a key: whoever holds it can claim the license. It's a random UUID that never leaves the family's devices except in calls to these endpoints and in Stripe or RevenueCat metadata.
Today src/lib/services/ai/GeminiService.ts calls Gemini straight from the app with VUE_APP_GEMINI_API_KEY, which ships the key inside the public bundle and hard-codes gemini-2.0-flash (COMPASS core already treats 2.0 as retired). XP Pro moves these calls to the server:
-
POST /compass/v1/gatekeeper/xp-pro/aiwith{ license_key, device_id, task, input }. The license must be entitled (3.2) and the device registered, or it's a402 {error:'pro_required'}. -
taskis one of the four the app uses today:monster_name,checklist(from a quest name),full_beast,schedule. The prompt templates move into PHP, one per task;inputis the task's structured fields only (names, short text, numbers), each length-capped. The endpoint never accepts a free-form prompt, so a Pro key can't be used as a general Gemini proxy. - The Gemini key and model come from COMPASS core's
googleconnector, the same resolutionXophz_Compass_Gemini_APIuses (its model fallback included). Responses are parsed to the JSON shape each task's caller expects today, and a malformed reply is a502. - Limit: 100 AI calls per license per day (a per-license transient counter),
429beyond that. The number is an option,xp_pro_ai_daily_limit.
| File | Kind | Responsibility |
|---|---|---|
src/lib/databases/ProDb.ts |
DB, @ionic/storage __xp_pro, like FamilySettingsDb
|
{ familyId, deviceId, license: { key, plan, expiresAt, env, lastValidatedAt } | null } |
src/lib/services/pro/proPlans.ts |
pure | Plan ids, display prices for the web, the gate list (templeCreator, advancedSchedule, aiHelpers) |
src/lib/services/pro/entitlement.ts |
pure |
isPro(cached, now, buildEnv), needsRecheck(cached, now); the offline rules in 4.4 |
src/lib/services/pro/familyIdentity.ts |
pure plus DB | Creates familyId (UUID v4 from crypto.randomUUID) and deviceId on first launch; adoptFamily(id) after a restore |
src/lib/services/pro/proClient.ts |
fetch | The endpoints; the site comes from src/lib/api/apiHost.ts (compassRestRoot(), compassSiteRoot()): the hosting WordPress site when xophz-compass-xp serves the app, otherwise https://doit.forthexp.com
|
src/lib/services/pro/storeBilling.ts |
native only |
@revenuecat/purchases-capacitor: configure({ apiKey, appUserID: familyId }), offerings, purchase, restore. Keys from VUE_APP_RC_IOS_KEY / VUE_APP_RC_ANDROID_KEY (public SDK keys) |
src/lib/services/ai/GeminiService.ts |
fetch (rewritten) | Same four exported functions and return shapes, now calling /xp-pro/ai; isGeminiConfigured() becomes "is Pro". VUE_APP_GEMINI_API_KEY is removed from the app and .env.example
|
src/lib/store/stores/pro.ts |
Pinia |
isPro, plan, loading; init(), purchase(plan), claim(), restoreWithKey(key), recover(email), manage(), requirePro(gate)
|
src/router/_Guards/pro.guard.ts |
guard | Routes with meta.pro = '<gate>' open the upsell instead of navigating when not Pro |
-
Go Pro (
/pro,XpGoPropage with sibling.ts/.scss). Styled as an SNES shop counter: the three plans as shop items, what Pro adds, "7 days free", Restore. On native, prices come from the RevenueCat offering (the store's local currency); on the web, from/xp-pro/plans. Lifetime shows only while it's on sale (web flag or present in the offering). -
Upsell modal (
XpProUpsell): opened byrequirePro(gate). Names the feature that was tapped, offers "See XP Pro" and "Not now". SoundopenMenuon open,noon dismiss. -
Return (
/pro/return): after Stripe sends the parent back. Pollsclaimevery 2 s for up to 30 s with a "the shopkeeper is counting your goldโฆ" dialog. On success: toast, then back to where the parent came from. On timeout: "Payment received; your unlock is on its way" with a Retry button, andinit()keeps trying in the background.status=cancelledreturns quietly to/pro. -
XP Pro panel in Settings (
XpSettings): status and plan, renewal or expiry date, the key masked with a copy button, "Restore with key", "Email me my key", and "Manage subscription" (manage()). Apple requires a visible Restore, so it calls RevenueCatrestorePurchases()on native and thenclaim().
Web. purchase(plan) sends the browser to {compassSiteRoot()}/buy/doitforthexp/{plan}?family_id=โฆ&device_id=โฆ&return_url={origin}/#/pro/return. Bazaar creates the Stripe session and redirects. The webhook grants; /pro/return claims.
Native. purchase(plan) โ storeBilling.purchase(package). On success, poll claim() exactly as /pro/return does: the RevenueCat webhook usually lands within a few seconds. The app never unlocks from the SDK's local result alone; Gatekeeper decides.
Restore on a new device. restoreWithKey(key) โ validate; on success adoptFamily(response.family_id) so this install shares the family's id, and RevenueCat gets logIn(familyId) on native so later store purchases credit the same family.
-
init()runs at app start. It loadsProDb, then callsvalidate(orclaimwhen there's no key yet) ifneedsRecheck: the last check was more than 24 hours ago, orexpiresAthas passed. -
Pro stays on offline while
lastValidatedAtis within 30 days and (expiresAtis null or now <expiresAt+ 3 days). - A network error keeps the cached state. A
200withpro:falseor a404turns Pro off but keeps the key, so a renewal or a support fix brings it back on the next check. - Production builds treat
env === 'sandbox'licenses as not Pro; development builds accept both. - This is client-side enforcement in a local-first app, so a determined user could patch it. That's acceptable for a family app at this price; Family Sync will be enforced on the server.
All gates block creating or editing a Pro feature. Nothing a family already has stops working when Pro is missing or lapses: custom temples stay playable and saved schedules keep firing. There is no limit on heroes, quests or beasts.
| Gate | Where | Free behavior |
|---|---|---|
templeCreator |
meta.pro = 'templeCreator' on the temple create and edit routes in src/router/Admin/temples.routes.ts (xp-room-editor and the temples/:templeId/* editors); the "New temple" button calls requirePro first |
Built-in temples play normally and are listed read-only. Custom beasts (bestiary.routes.ts) and custom quests are free and ungated |
aiHelpers |
The AI buttons in BeastDetailsModal.vue (name, checklist), QuestTypeTab.vue (full beast) and AchievementScheduleTab.vue (schedule) call requirePro('aiHelpers') before calling GeminiService
|
The buttons show with a padlock and open the upsell; everything they would fill in can still be typed by hand |
advancedSchedule |
AchievementScheduleTab.vue |
Free: basicSchedule once, daily (one time a day) and weekly with "which days". Pro: custom (repeat and interval), the times-of-day chips, and "times per day" above 1. Locked controls show a small padlock and open the upsell when tapped |
- Stripe: a live and a test webhook endpoint pointing at the Hookshot receive URL, with the six events in 3.3. The Stripe signing secret goes into the Hookshot webhook's signing secret, header
Stripe-Signature, modehmac. Billing Portal turned on in the Stripe dashboard. - RevenueCat: project with the iOS and Android apps; entitlement
pro; productsxp_pro_monthly,xp_pro_yearly,xp_pro_lifetime; one offering with all three. Webhook to the Hookshot receive URL with an Authorization value ofBearer <secret>; the same secret goes into the Hookshot webhook, modebearer. - App Store Connect and Play Console: the three products at $4.99, $39.99 (both with a 7-day free introductory offer) and $79, in one subscription group.
- WordPress (
doit.forthexp.com): activate Xophz COMPASS, Bazaar, Gatekeeper, Hookshot and XP. Checked 2026-10-12: Hookshot, Bazaar, Gatekeeper (26.10.12-384) and XP are active. Bazaar's/buy/โฆrouter works but falls back to the checkout simulator because no Stripe secret key is set in Settings > Connectors. The front end shows a maintenance page; REST and/buy/โฆare unaffected. Also set the WordPress Address (siteurl, currentlyhttp://) tohttps://to match the Site Address. The site answers through Cloudflare, so defineXOPHZ_HOOKSHOT_TRUSTED_PROXIESinwp-config.phpwith Cloudflare's published IP ranges; otherwise every visitor shares one rate-limit bucket. Then adddo-it-for-the-xp.pages.dev(and the production domain once there is one) tocompass_trusted_redirect_domains; setgatekeeper_xp_pro_webhooks;xp_pro_lifetime_on_sale = true. - App
.env(and.env.example):VUE_APP_RC_IOS_KEY,VUE_APP_RC_ANDROID_KEY. The WordPress site needs no setting:apiHost.tsalready resolves it.
-
App, jest (
chemx test):entitlement.spec.ts(lifetime, active, inside the 3-day grace, expired, 30-day offline window, sandbox in a production build);familyIdentity.spec.ts(created once, stable, adopt);pro.store.spec.tswith a mockedproClient(claim polling stops on success and on timeout; restore adopts the family; network error keeps Pro);GeminiService.spec.tswith a mockedproClient(each function sends its task and fields, and maps the reply to today's return shape;pro_requiredsurfaces as the upsell). -
App, Cypress:
pro.spec.jsโ withcy.interceptstubbingclaim,validateandplans: a fourth kid can be added on the free plan;/pro/returnunlocks; an AI button opens the upsell when free; the Temple Creator route is blocked when free and open when Pro. -
Server, COMPASS PHP harness (
xophz-compass/tests/harness, run withnode wp-content/plugins/<plugin>/tests/run-tests.mjs; PHP runs inside theu-wordpresscontainer): the decision tables in the two adapters are pure static methods (event in, operation out) tested row by row with real-shaped Stripe and RevenueCat JSON for every row of the tables in 3.3, plus a replayed event (no double grant), an unverified webhook (dropped), and a wrong webhook id (dropped). The AI proxy: no license (402), expired license (402), unknown task (400), over-long input (400), the daily limit (429), and a canned Gemini reply per task mapped to the expected JSON. Hookshot gets its firsttests/run-tests.mjsfor thebearermode. -
End to end, manual before launch: Stripe test mode from
localhost:8100(Bazaar picks the test key on its own) through webhook, claim, renewal (Stripe test clock), cancel and refund; RevenueCat sandbox on an iOS and an Android device; restore on a second device with the key.
Family Sync and multiplayer; accounts and passwords; price experiments; ads; coupons beyond Stripe promotion codes (Bazaar already turns them on); in-app purchases of consumables or cosmetics; moving existing CX- licenses.
- Hookshot
bearerverify mode. - Gatekeeper: license store, both adapters, intake, endpoints, fixture runner, admin listing; then the AI proxy (3.5).
- XP plugin: catalog and
/buy/doitforthexpresolver;/xp-pro/plans. - App:
ProDb, pure services and specs,proClient, Pinia store, Go Pro and Return screens, Settings panel, web purchase. - App: the gates, and
GeminiServicemoved onto the AI proxy. - App: RevenueCat native purchase and restore (needs the store products from section 5).
- Docs:
docs/XP-Membership.mdrewritten from this spec; Gatekeeper, Hookshot and XP plugindocs/CHANGELOG.mdentries.