feat(ads): ChatGPT ad images and direct offer (money-back guarantee, no trial) for paid-ads visitors
Why
Paid-ads visitors get a pay-first offer: no free trial, backed by a 14-day money-back guarantee. Every ad URL will carry offer=direct.
Ships together with the backend. The backend part (no trial for signups whose metadata has offer: 'direct') lands separately in the Cap-go/capgo repo. Both PRs must ship together. If this goes out alone, ad visitors will see "money-back guarantee" copy but still get the trial. If the backend goes out alone, nothing changes because no signup sends offer.
What
Detection and persistence (apps/web/src/lib/attribution.ts, Attribution.astro)
offerquery param is captured only when its value is exactlydirect. Any other value is ignored, both from the URL and from stored data.- It is stored with the same first-touch cookie + localStorage mechanism as
utm_*, so it survives going from the landing page to/register/. Once set, it stays set (first touch, 180 days). getSignupUserMetadataForRegistration()addsoffer: 'direct'. Since #1235,register.tsspreads it into thePOST /auth/registerbody.RegisterUserInputdeclaresoffer?: 'direct', and the backend schema in Cap-go/capgo.app#3782 accepts an optionalofferand only acts ondirect.getSignupAttributionForEvents()and PostHog attribution props addsignup_offer: 'direct'.- Link decoration also forwards
offer=directon register and console links. - The synchronous inline head script sets
<html data-offer="direct">before first paint, either from the URL or from stored attribution.
Copy swap without a flash
- In
global.css,[data-offer='direct'] [data-trial-copy]is hidden, andhtml:not([data-offer='direct']) [data-direct-copy]is hidden. Both variants are static HTML, and CSS shows exactly one of them. - The new
OfferText.astrocomponent renders the trial text plus its direct variant. - Normal visitors get the same visible copy as before. JSON-LD and FAQ data are untouched.
| Where | Normal visitor | offer=direct |
|---|---|---|
| Header CTA (desktop + mobile) | Start for free | Get started |
SignupCta button (hero + final CTA on all pages using it) | Get started for free | Get started |
SignupCta note | 14-day unlimited free trial. No credit card required | 14-day money-back guarantee. Cancel anytime. |
/solutions/[builder]-to-mobile/ CTA subtitle | Start the 14-day free trial, export your {builder} project, … | Pick a plan with a 14-day money-back guarantee, export your {builder} project, … |
/solutions/webapp-to-mobile/ CTA subtitle | Start the 14-day free trial, wrap your app with Capacitor, … | Pick a plan with a 14-day money-back guarantee, wrap your app with Capacitor, … |
/solutions/build-without-mac/ CTA subtitle | Run your first signed iOS build during the 14-day free trial, … | Run your first signed iOS build with a 14-day money-back guarantee, … |
/live-update/ comparison "Try before you pay" | 14-day unlimited free trial. No credit card required. | 14-day money-back guarantee. Cancel anytime. |
/live-update/ final CTA meta | 14-day unlimited free trial | 14-day money-back guarantee |
/native-build/ comparison hook | Yes. 14-day free trial, no credit card. | Yes. 14-day money-back guarantee. |
/native-build/ pricing lead | … Start with a 14-day free trial, then pick monthly or annual billing. | … Pick monthly or annual billing, with a 14-day money-back guarantee. |
/native-build/ plan buttons | 14-day free trial | Get started |
/native-build/ final CTA meta | No credit card to start · 14-day free trial · Cancel anytime | 14-day money-back guarantee · Cancel anytime (credit-card line hidden) |
/register/ pills | 14-day unlimited free trial · No credit card required | 14-day money-back guarantee · Cancel anytime |
/pricing/ H1 | 14 days free, no credit card. Scale to millions. | 14-day money-back guarantee. Scale to millions. |
/pricing/ plan buttons (Plans.astro) | 14-day unlimited free trial (a11y name "Start {plan} free trial") | Get started (a11y name "Get started with the {plan} plan") |
/pricing/ calculator CTAs (PricingCalculator.astro, desktop + mobile bar) | 14-day unlimited free trial | Get started |
/pricing/ "How pricing works" highlight (PricingExplainer.astro) | 14-day trial, no credit card | 14-day money-back guarantee |
/pricing/ FAQ section title / subtitle | Trial & Support / Free trials, discounts, and getting help | Guarantee & Support / Money-back guarantee, discounts, and getting help |
/pricing/ FAQ answer "unsure about active users" (visible only, JSON-LD unchanged) | …take advantage of Capgo's free trial… | …pick the plan that matches your best estimate. Every plan comes with a 14-day money-back guarantee… |
New message keys are in apps/shared/copy/messages.ts: direct_offer_cta_get_started, direct_offer_guarantee, direct_offer_guarantee_note, direct_offer_cancel_anytime, *_cta_subtitle_direct (×3), native_build_v2_comp_try_capgo_hook_direct, native_build_builder_pricing_text_direct, plans_that_scale_with_your_business_direct, pricing_explainer_trial_direct, free_trial_explanation_direct, pricing_faq_trial_support_title_direct, pricing_faq_trial_support_subtitle_direct, direct_offer_plan_cta_aria. Contexts were regenerated with bun run generate:message-contexts.
Verification
After rebasing onto main (#1235, #1239 API signup + Turnstile, #1240 TS 7 + oxlint):
bun run lint(oxlint): exit 0. No new warnings in changed files; the one incheck-contrast.mjs(no-useless-escape) was already there.- Tests: the CI set (abtest, unifiedRss, attribution,
scripts/register.test.ts,services/registration.test.ts) plus pricing-faq: 42 pass. The register tests assertoffer: 'direct'in the API request body andsignup_offeronuser_signed_up. astro check0 errors,build:webOK,seo:check0 errors / 0 warnings,contrast:checkpassed.- Contrast check: plan CTAs now carry a stable
data-plan-ctahook. The old selector matched the trial-specificaria-label, which can't be correct for both variants. The check measures the visible label variant on/pricing/and again on/pricing/?offer=direct. - Turnstile: built with Cloudflare's always-pass test site key. The widget renders on
/register/for both normal and direct-offer visitors, below the swapped badges (see the refreshed screenshot).
Before the rebase:
bun test apps/web/test/attribution.test.js: 25 pass. New tests cover offer parsing and validation, persistence and merge, deserialization, PostHog props, link forwarding, and registration metadata/events with stored and URL offer.cd apps/web && bun run check(astro check): 0 errors, 0 warnings.bun run build:web: OK.bun run seo:check: 0 errors, 0 warnings.- Headless Chrome against the built
dist:/register/with a fresh profile has nodata-offer.- After visiting
/live-update/?offer=direct,/register/(no param) getsdata-offer="direct"and shows the guarantee pills and the "Get started" header. ?offer=foogets no flag.- The lovable-to-mobile hero shows "Get started for free / 14-day unlimited free trial…" normally and "Get started / 14-day money-back guarantee. Cancel anytime." with
?offer=direct.
/pricing/screenshots with and without the direct offer (persisted from a lovable-to-mobile ad landing) look correct. The HTML scan of/pricing/finds only the payment-methods FAQ ("We accept all major credit cards"), which is not trial copy.- An HTML scan of the six target pages finds no trial wording outside
data-trial-copyelements (scripts/JSON-LD excluded).
Screenshots
Built from this branch and served locally. Captured with headless Chrome at 1440px, with a fresh profile per capture. Left: normal visitor. Right: ?offer=direct.
/solutions/lovable-to-mobile/: hero

/solutions/lovable-to-mobile/: final CTA

/live-update/: final CTA

/native-build/: pricing and plan buttons

/pricing/: headline and plan buttons

/register/: fresh visit vs. reached after visiting /solutions/lovable-to-mobile/?offer=direct (no query param on /register/)

Ad images (previously #1233)
27 square ad images (1080×1080 PNG) for the ChatGPT ad campaigns, one per ad. No image is used twice:
- 5 builder pages from #1226 (Lovable, Base44, Bolt, Replit, v0) × 3 angles: store listing, no Mac, live updates.
- 4 use-case pages × 3 angles:
/live-update/: ship fixes without review, upload command, rollback./native-build/: one command, cloud iOS builds, QR sharing./solutions/build-without-mac/: no Mac, ship from Windows, credentials deleted after the build./solutions/webapp-to-mobile/: store listing, one codebase, cloud builds.
The CTA on each image says "Get started" with "14-day money-back guarantee" under it. That matches the offer=direct ad offer: no free trial, money-back guarantee instead. Text on the images only repeats claims the landing pages already make.
Images are served at https://capgo.app/ads/chatgpt/{campaign}-{variant}.png. docs/ads/chatgpt-campaigns.md lists the 9 campaigns with each ad's title, copy, image and URL.
Ad image screenshots
Each row is one campaign; the columns are ads A, B and C.
| Campaign | A | B | C |
|---|---|---|---|
| Lovable | ![]() | ![]() | ![]() |
| Base44 | ![]() | ![]() | ![]() |
| Bolt | ![]() | ![]() | ![]() |
| Replit | ![]() | ![]() | ![]() |
| v0 | ![]() | ![]() | ![]() |
| Live Update | ![]() | ![]() | ![]() |
| Native Build | ![]() | ![]() | ![]() |
| Build without Mac | ![]() | ![]() | ![]() |
| Web app to mobile | ![]() | ![]() | ![]() |
🤖 Generated with Claude Code
Summary by CodeRabbit
-
New Features
- Added direct-offer messaging across pricing, signup, navigation, FAQs, and solution pages.
- Visitors arriving with
offer=directnow see a 14-day money-back guarantee and cancellation terms instead of free-trial messaging. - Direct-offer attribution is retained and included in registration and analytics data.
- Added localized “Get started” labels and accessible plan-specific CTA text.
-
Documentation
- Added ChatGPT advertising campaign guidance, landing pages, tracking links, and creative specifications.
-
Tests
- Expanded coverage for direct-offer attribution and pricing CTA validation.


























