FREE SAMPLE — BLUEPRINT 00 · THE FULL FORMAT, NOTHING CUT

1. Product brief

The Validation Machine is not a product you sell. It is the instrument you build once and point at every idea you have afterwards. It answers a single question with numbers instead of vibes: does anyone want this enough to pay before it exists?

It has four parts and no fifth: a landing page making exactly one promise, a double-opt-in waitlist, a real Stripe pre-order checkout with an unconditional refund promise, and a readout that turns visits → emails → pre-orders into a verdict string.

Who this is for and why. The failure mode it kills is the expensive one: three months building, then launch, then silence. The usual "validation" substitutes — asking friends, posting a poll, a Typeform, counting GitHub stars — measure politeness, not demand. Email addresses are cheap to give. The only signal in the funnel that costs the visitor something real is a card charge. That is why the pre-order is load-bearing and why a blueprint that stops at "collect emails" is a worse instrument.

The ONE core loop.

one promise → visit measured → email captured → email confirmed
            → pre-order charged → funnel read → SHIP or KILL

Everything else is v2. The instrument's value is entirely determined by whether its numbers are trustworthy, which is why Milestones 3, 5 and 6 are about measurement integrity rather than features.

The honest hard part. Most people build the page and then discover they have no traffic, so the funnel reads 40 visits and nothing is learnable. The build is the easy half. Milestone 7 is the other half, and skipping it wastes the whole exercise.

Pricing

You are not selling this. You are pricing the pre-order on the page it hosts, and that number is the measurement itself.

Charge 40–60% of your intended launch price, in full, today, with a no-questions refund until launch day. If you intend to charge $20/month, pre-sell a year at $99. If it is a one-off tool at $79, pre-sell at $39.

Reasoning: a $1 "commitment" measures nothing — people spend $1 to be nice. A price that requires a two-second pause is the entire experiment. Charging in full (not $1 authorizations, not "reserve your spot") is what makes the number honest, and the unconditional refund is what makes it fair. Expect and budget for refunds; a refund is a successful measurement, not a loss.

Assumption, stated as such: somewhere around 40–60% of intended price is the zone where the decision is real but the buyer isn't gambling. This is judgement, not measured data. If your intended price is above ~$300, pre-sell a deposit instead and say plainly that it is a deposit.

2. Scope contract

v1 IN

v1 OUT — these are prohibitions, not suggestions

3. Architecture

Decided, with reasons. Do not substitute.

Data flow

browser  ──GET /──────────► static page (no JS blocking render)
         ──POST /api/e ───► view event      (visitor_key computed server-side)
         ──POST /api/wait─► subscriber(pending) + Resend confirmation
         ──POST /confirm ─► subscriber(confirmed) + confirm event
         ──GET buy.stripe.com/... ?client_reference_id=<visitor_key>&prefilled_email=...
Stripe   ──POST /api/stripe/webhook ─► checkouts row + checkout_paid event
you      ──GET /admin ─────► funnel + per-channel + verdict

4. Data model

Four tables. Every field earns its place.

-- Rotating salt so visitor hashes are stable within a day and unlinkable across days.
-- Shared state, because serverless instances do not share memory.
create table daily_salt (
  day   date primary key,
  salt  text not null                       -- 32 random bytes, hex
);

-- Every measured moment. One table, because the funnel is one ordered sequence.
create table events (
  id          bigserial primary key,
  ts          timestamptz not null default now(),
  kind        text        not null check (kind in
                ('view','signup','confirm','checkout_start','checkout_paid')),
  visitor_key char(32)    not null,         -- daily hash; never an IP
  channel     text        not null default 'direct',  -- normalized utm_source
  dedupe_key  text,                         -- stripe event id, token id, etc.
  meta        jsonb       not null default '{}'::jsonb
);
create unique index events_dedupe on events (kind, dedupe_key) where dedupe_key is not null;
create index events_ts   on events (ts);
create index events_kind on events (kind, visitor_key);

-- The waitlist. Lowercased text + unique beats citext: no extension to install.
create table subscribers (
  id            bigserial   primary key,
  email         text        not null unique,        -- always stored lowercased, trimmed
  status        text        not null default 'pending'
                  check (status in ('pending','confirmed','unsubscribed','bounced')),
  token_hash    char(64)    not null,               -- sha256 of the confirm token
  token_expires timestamptz not null,               -- now() + 7 days
  visitor_key   char(32),                           -- attribution, nullable
  channel       text        not null default 'direct',
  created_at    timestamptz not null default now(),
  confirmed_at  timestamptz
);

-- Paid pre-orders, written only by the verified webhook.
create table checkouts (
  id                bigserial   primary key,
  stripe_session_id text        not null unique,    -- idempotency vs. Stripe retries
  stripe_event_id   text        not null unique,    -- idempotency vs. duplicate delivery
  email             text,
  amount_total      integer     not null,           -- minor units, as Stripe sends
  currency          char(3)     not null,
  visitor_key       char(32),                       -- from client_reference_id
  channel           text        not null default 'direct',
  status            text        not null default 'paid'
                      check (status in ('paid','refunded')),
  created_at        timestamptz not null default now()
);

Why the raw confirm token is not stored: a leaked database read should not let anyone confirm addresses they do not control. The token exists in exactly one place, the email.

Why amount_total is an integer of minor units: Stripe sends minor units. Converting to a float on the way in is how you end up with $38.99999 in your readout.

5. Milestones

M1 — Skeleton, schema, deployed on day one

Goal: A live URL on your real domain, backed by a real database, before any feature exists.

Agent prompt:

Create a new Next.js project using the App Router and TypeScript, styled with Tailwind, in this
directory. It will be deployed to Vercel and backed by a Neon Postgres database accessed through
Drizzle ORM using the Neon serverless HTTP driver.

Set up the four tables I will paste after this message as a Drizzle schema in TypeScript, and wire
drizzle-kit so I can push the schema to the database. Important: the app's runtime database
connection must use the pooled Neon connection string, but drizzle-kit must be configured to use a
separate unpooled connection string, because the CLI connects over plain TCP and fails against a
pooled URL.

Add a health endpoint that performs one trivial round-trip query against the database and returns
JSON containing an ok boolean, the current database time, and the count of rows in the events table.
It must return HTTP 503 with ok false if the query fails, never a 500 stack trace.

Read every secret from environment variables. Create an .env.example listing every variable name with
an empty value and a one-line comment. Never commit real values. Add a README section listing the
exact commands to install, push the schema, run locally, and deploy.

Produces: app/api/health/route.ts, db/schema.ts, db/client.ts, drizzle.config.ts, .env.example, a Vercel project on your domain.

Verification gate:

npx drizzle-kit push                 # expect: 4 tables created, no pooled-URL warning
curl -s https://YOURDOMAIN/api/health | jq
# expect exactly: {"ok":true,"dbTime":"<ISO timestamp>","events":0}
curl -s -o /dev/null -w '%{http_code}\n' https://YOURDOMAIN/
# expect: 200

Then, in the Neon console, break it on purpose: pause the database, re-run the health curl, expect 503 and "ok":false. Resume. A health check that cannot fail is not a health check.


M2 — The one-promise landing page

Goal: A page that states one promise, offers one email field and one pre-order button, and is mechanically prevented from growing a second idea.

Agent prompt:

Build the single landing page for this project. Its entire job is to make one promise clearly enough
that a stranger decides in under fifteen seconds whether they want it.

Structure, in order: one headline stating the promise in twelve words or fewer; one subheading of at
most two sentences saying who it is for and what changes for them; three short bullets of concrete
specifics, no adjectives like "powerful" or "seamless"; the email capture form; the pre-order button
with the price and a refund promise directly beneath it in plain language; a one-line footer with a
mailto contact address. Nothing else. No navigation bar, no testimonials, no logo wall, no FAQ, no
countdown timer, no second call to action.

Constraints: it must render fully without JavaScript, use only the system font stack and Tailwind
defaults with a single accent color, and score well on mobile at 375px width. The page must be
statically rendered.

Also write an automated test asserting the page's structure so it cannot drift: exactly one h1,
exactly one form element, exactly one external payment link, no anchor tags to any other page on this
site, and a headline of twelve words or fewer. This test is a permanent guard, not a one-off check.

Produces: app/page.tsx, app/layout.tsx, tests/page-contract.test.ts.

Verification gate:

npm test -- page-contract
# expect: all assertions pass

curl -s https://YOURDOMAIN/ | grep -c '<h1'          # expect: 1
curl -s https://YOURDOMAIN/ | grep -c '<form'        # expect: 1
curl -s https://YOURDOMAIN/ | grep -oc 'buy.stripe.com'  # expect: 1

Human gate, and it is not optional: open the page on a phone, hand it to someone who does not know what you are building, and ask "what does this do and who is it for?" If they cannot answer in one sentence, the promise is the defect — not the page, not the funnel. Fix it before Milestone 3.


M3 — Cookieless visit measurement you can trust

Goal: Every human visit is counted once per day, bots are excluded, and no IP is ever stored.

Agent prompt:

Add first-party visit measurement to this app. No third-party analytics service, no cookies, no
localStorage.

Server-side, derive a visitor key by hashing a per-day secret salt together with the request's client
IP and user-agent string, keeping the first 32 hex characters. The raw IP must never be written to the
database or to logs. The per-day salt must be stored in the database table provided, generated on
first use of each day, and salts older than two days must be deleted whenever a new one is created.

Expose a small POST endpoint at the path /api/e that records a view event. Call it from the landing
page with a beacon-style request after first paint. Record the traffic channel from the utm_source
query parameter, normalized to lowercase, defaulting to the string "direct". A given visitor key must
produce at most one view event per day — enforce that in the database with the dedupe column, not with
application logic that can race.

Exclude obvious non-humans: reject requests whose user-agent matches common crawler, preview, and
uptime-monitor patterns, and reject requests with no user-agent at all. Return 204 for both accepted
and rejected requests so a caller learns nothing about the filter.

The endpoint must never trust a client-supplied visitor identifier. It is computed on the server only.

Produces: app/api/e/route.ts, lib/visitor.ts, lib/bots.ts, components/beacon.tsx.

Verification gate:

# 1. Same client, twice, counts once.
for i in 1 2; do curl -s -o /dev/null -w '%{http_code} ' -X POST https://YOURDOMAIN/api/e \
  -H 'content-type: application/json' -H 'user-agent: Mozilla/5.0 (gate-test)' \
  -d '{"kind":"view"}'; done; echo
# expect: 204 204
# then, in the Neon SQL editor:
#   select count(*) from events where kind='view' and ts > now() - interval '5 minutes';
# expect: exactly 1 — two identical requests, one counted visit

# 2. A crawler is not a visitor.
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://YOURDOMAIN/api/e \
  -H 'user-agent: Mozilla/5.0 (compatible; Googlebot/2.1)' -d '{"kind":"view"}'
# expect: 204, and NO new row in events

# 3. No IP anywhere. In the Neon SQL editor:
#   select column_name from information_schema.columns where table_name='events';
# expect: the returned list contains no column named ip, ip_address, or remote_addr
#   select count(*) from events where meta::text ~ '\d+\.\d+\.\d+\.\d+';
# expect: 0

M4 — Double opt-in waitlist

Goal: An email address in the database means a human typed it and then clicked to confirm it.

Agent prompt:

Implement the waitlist with genuine double opt-in, using Resend for delivery from a domain I have
verified with SPF, DKIM and DMARC records.

On form submission: validate and normalize the address to lowercase and trimmed, insert or update a
subscriber row in pending status, generate a high-entropy random confirmation token, store only its
SHA-256 hash plus an expiry seven days out, and email the raw token as a confirmation link. Record a
signup event attributed to the same visitor key and channel used by the view events. Re-submitting an
address that is already pending must resend the same-shaped email with a fresh token rather than
creating a duplicate row, and re-submitting an already-confirmed address must respond identically to a
new signup so the endpoint cannot be used to test whether an address is on the list.

The confirmation link must open a page with a single visible confirm button that submits a POST. Do
not confirm on the GET. Corporate mail scanners and link-preview bots fetch every URL in an inbound
email, and a GET-confirm endpoint will silently mark those addresses confirmed, inflating the exact
number this whole project exists to measure. On successful confirm, mark the row confirmed, stamp the
time, invalidate the token, and record a confirm event. An expired, unknown, or already-used token
must render a plain "this link is no longer valid, request a new one" page, never an error page.

Abuse controls: a honeypot field that real users never see and bots fill, rejection of submissions
made under two seconds after page load, and a limit of five signups per visitor key per hour enforced
by a database count rather than an in-memory counter.

Resend's free tier allows 100 emails per day. If a send is rejected for quota or rate reasons, still
create the pending row, mark that the confirmation is undelivered in the row's metadata, log it
loudly, and return success to the user. Losing the address is worse than a delayed email.

Produces: app/api/wait/route.ts, app/confirm/[token]/page.tsx, app/api/confirm/route.ts, lib/email.ts, emails/confirm.tsx.

Verification gate:

# 1. Signup creates a pending row and sends one email.
curl -s -X POST https://YOURDOMAIN/api/wait -H 'content-type: application/json' \
  -d '{"email":"gate+1@yourdomain.com","hp":"","t":9999}'
# expect: {"ok":true}
#   select status, confirmed_at from subscribers where email='gate+1@yourdomain.com';
# expect: pending | null

# 2. The scanner test — a GET must NOT confirm.
curl -s -o /dev/null "https://YOURDOMAIN/confirm/<TOKEN_FROM_EMAIL>"
#   select status from subscribers where email='gate+1@yourdomain.com';
# expect: still pending

# 3. The button POST confirms exactly once.
curl -s -X POST https://YOURDOMAIN/api/confirm -H 'content-type: application/json' \
  -d '{"token":"<TOKEN_FROM_EMAIL>"}'      # expect: {"ok":true}
curl -s -X POST https://YOURDOMAIN/api/confirm -H 'content-type: application/json' \
  -d '{"token":"<TOKEN_FROM_EMAIL>"}'      # expect: {"ok":false,"reason":"invalid"}
#   select status, count(*) from subscribers where email='gate+1@yourdomain.com' group by status;
# expect: confirmed | 1
#   select count(*) from events where kind='confirm';    -- increased by exactly 1

# 4. Enumeration is not possible: the response to a brand-new address and to an
#    already-confirmed one must be byte-identical.

M5 — The pre-order checkout and the webhook that proves it

Goal: A real card charge lands in Stripe and in your database exactly once, attributed to the visitor who made it.

Create the Payment Link in the Stripe dashboard, in test mode first: one-time price at your pre-order amount, "After payment → Don't show confirmation page", redirect to https://YOURDOMAIN/thanks?session_id={CHECKOUT_SESSION_ID}. Stripe substitutes the real session id, and any UTM parameters on the inbound link are carried through to that redirect automatically.

Agent prompt:

Wire the pre-order flow around an existing Stripe Payment Link whose base URL comes from an
environment variable.

On the landing page, build the outbound link by appending the current visitor key as the
client_reference_id parameter, and, when the visitor has already submitted an email this session,
their address as the prefilled_email parameter, percent-encoded. Preserve any utm_source from the
inbound URL. Record a checkout_start event at click time. Note that client_reference_id accepts only
alphanumerics, dashes and underscores up to 200 characters, and that invalid values are silently
dropped — the visitor key format must comply.

Implement the webhook endpoint that receives checkout.session.completed. It must run on the Node
runtime, read the request body as raw unmodified text before any JSON parsing, and verify the Stripe
signature header against the endpoint's signing secret using the SDK's asynchronous verification
helper. A missing, malformed, or invalid signature returns 400 and writes nothing. Only after
successful verification may the payload be trusted.

On a verified completed session, insert a checkout row recording the session id, the Stripe event id,
the customer email, the total in minor units exactly as sent, the currency, and the visitor key taken
from client_reference_id. Both the session id and the event id are unique keys: a duplicate delivery
of either must be a no-op that still returns 200, because Stripe retries and will send the same event
again if you do not. Record a matching checkout_paid event. Unhandled event types return 200 without
writing.

Also write an offline test that constructs a signed payload with the SDK's test-header helper and
asserts three things: a valid signature results in exactly one checkout row; replaying the identical
event results in still exactly one row; and a payload whose body has been altered by a single
character is rejected with 400 and writes nothing. This test must require no network and no Stripe
account.

Finally, add the thank-you page the redirect points at. It reads the session id from the query string,
displays a confirmation and the refund promise, and must not itself write any payment record — the
webhook is the only writer.

Produces: app/api/stripe/webhook/route.ts, app/thanks/page.tsx, lib/stripe.ts, tests/webhook.test.ts.

Verification gate:

# 1. Offline, deterministic, no Stripe account needed:
npm test -- webhook
# expect 3 passing: valid → 1 row; replay → still 1 row; tampered body → 400 and 0 rows

# 2. Live against test mode, in two terminals:
stripe listen --events checkout.session.completed --forward-to localhost:3000/api/stripe/webhook
stripe trigger checkout.session.completed
# expect in the listen terminal: [200 POST /api/stripe/webhook]
#   select count(*) from checkouts;                 -- increased by exactly 1
#   select count(*) from events where kind='checkout_paid';  -- increased by exactly 1

# 3. Signature enforcement against the deployed URL:
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://YOURDOMAIN/api/stripe/webhook \
  -H 'content-type: application/json' -H 'stripe-signature: t=1,v1=deadbeef' \
  -d '{"type":"checkout.session.completed"}'
# expect: 400, and no new row in checkouts

# 4. Attribution end to end: open your own landing page, click the pre-order button, pay with
#    test card 4242 4242 4242 4242, then:
#   select visitor_key, amount_total, currency from checkouts order by id desc limit 1;
# expect: a 32-char visitor_key matching a view event from the same browser, and the exact
#         minor-unit amount you configured (e.g. 3900 for $39.00)

M6 — The readout

Goal: One private page that shows the funnel, splits it by channel, and prints a verdict you did not get to argue with.

Agent prompt:

Build the private readout page at /admin, protected by a single shared secret supplied as a query
parameter on first visit, compared in constant time, and then held in a signed httpOnly cookie for
thirty days. No login form, no user table. Any unauthenticated request gets a 404, not a 401 — an
unauthenticated visitor should not learn the page exists.

The page shows, for a date range defaulting to the current run:

  - The funnel as absolute counts and as conversion percentages between adjacent steps: unique
    visitors, email signups, confirmed emails, checkout starts, paid pre-orders.
  - The same funnel broken down by channel, sorted by visitor count, so a single referral spike is
    visible as its own row rather than hidden in the total.
  - Days elapsed in the run, number of distinct channels that produced at least twenty visitors, and
    gross pre-order revenue in the currency Stripe reported.
  - A single verdict banner produced by a pure, unit-tested function that takes the counts and returns
    one of SHIP, KILL, or INCONCLUSIVE plus a one-sentence reason.

The verdict function's rules, which must live in code as named constants with a comment marking every
threshold as an assumption rather than a measured fact:

  INCONCLUSIVE if fewer than 400 unique visitors, or fewer than 7 days elapsed, or fewer than two
  channels reached twenty visitors. Reason names the specific shortfall.
  KILL if the sample is sufficient and confirmed emails are under 2% of unique visitors.
  SHIP if the sample is sufficient and confirmed emails are at least 8% of unique visitors and there
  are at least five paid pre-orders.
  INCONCLUSIVE otherwise, with the reason naming which of the two conditions fell short.

Write a seed script that wipes and inserts a fixed fixture dataset, plus unit tests over the verdict
function covering each branch and the boundary values exactly at the thresholds.

Produces: app/admin/page.tsx, lib/funnel.ts, lib/verdict.ts, scripts/seed.ts, tests/verdict.test.ts, middleware.ts.

Verification gate:

npm test -- verdict
# expect: every branch and both boundary values pass

npm run seed -- --scenario=kill
# fixture: 900 visitors, 12 confirmed, 0 paid, 10 days, 3 channels
curl -s "https://YOURDOMAIN/admin?k=$ADMIN_KEY" | grep -o 'KILL'          # expect: KILL

npm run seed -- --scenario=ship
# fixture: 900 visitors, 95 confirmed, 7 paid, 10 days, 3 channels
curl -s "https://YOURDOMAIN/admin?k=$ADMIN_KEY" | grep -o 'SHIP'          # expect: SHIP

npm run seed -- --scenario=thin
# fixture: 120 visitors, 30 confirmed, 4 paid, 3 days, 1 channel
curl -s "https://YOURDOMAIN/admin?k=$ADMIN_KEY" | grep -o 'INCONCLUSIVE'  # expect: INCONCLUSIVE

curl -s -o /dev/null -w '%{http_code}\n' https://YOURDOMAIN/admin         # expect: 404
curl -s -o /dev/null -w '%{http_code}\n' "https://YOURDOMAIN/admin?k=wrong"  # expect: 404

npm run seed -- --scenario=clean   # wipe fixtures before the real run; verify counts are 0

M7 — Launch, the 10-day run, and the kill/ship protocol

Goal: Real strangers, real money, a real verdict, and refunds honored if the answer is no.

Agent prompt:

Prepare this project for its live run and make the run itself operable.

Switch every Stripe value from test to live: the Payment Link URL, the secret key, and a webhook
endpoint registered in live mode whose signing secret differs from the test one. Confirm the live
Payment Link's redirect still points at the thank-you page with the session id placeholder intact,
because that setting does not carry over between modes.

Add a command that prints the current funnel and verdict as a compact line of text so I can check the
run from a terminal without opening a browser.

Add a refund command that lists every paid pre-order with its Stripe payment identifier and amount,
and, given explicit confirmation, refunds them all in full and marks each checkout row refunded. This
is not an optional convenience: the page promises unconditional refunds, and a KILL verdict obligates
me to issue them. It must be idempotent and must never refund the same charge twice.

Add a public plain-text page at /promise stating the refund terms, and link it beneath the pre-order
button.

Produces: live Stripe wiring, scripts/status.ts, scripts/refund-all.ts, app/promise/page.tsx.

Verification gate:

# The only gate that matters: buy your own product with a real card, in live mode.
# Then, immediately:
npm run status
# expect one line, e.g.: visits=1 signups=0 confirmed=0 starts=1 paid=1 rev=39.00USD
#                        days=0 channels=0 verdict=INCONCLUSIVE(sample: 1/400 visitors)

#   select status, amount_total from checkouts order by id desc limit 1;   -- paid | 3900

npm run refund-all -- --dry-run     # expect: lists exactly that one charge, refunds nothing
npm run refund-all -- --confirm     # expect: 1 refunded
npm run refund-all -- --confirm     # expect: 0 refunded (idempotent)
#   select status from checkouts order by id desc limit 1;                 -- refunded
# and the refund appears in the Stripe dashboard within a minute

The run itself: 10 days minimum, no changes to the page mid-run. Changing the headline on day 4 does not give you two data points, it gives you zero. Check npm run status once a day and otherwise leave it alone.

After the verdict:

Assumptions in the rubric, stated plainly: the 400-visitor floor, the 2% and 8% confirmed-email bands, and the five-pre-order minimum are judgement calls chosen so the sample is large enough that a few friends cannot swing it and the gap between KILL and SHIP is wide enough to be meaningful. They are not derived from an industry benchmark, and any benchmark you are quoted for "average landing page conversion" is measuring a different page, a different price, and different traffic. Write your own thresholds down before the run starts, in the code, where you cannot renegotiate them at 2am on day nine when the number comes in at 6%.

6. Security & production checklist

7. Launch checklist

  1. Domain and DNS. Real domain, not a .vercel.app subdomain — the deployment platform's name in your URL measurably changes how a stranger reads the promise. Apex plus www redirecting to apex.
  2. Email domain auth. SPF, DKIM and DMARC records verified in Resend before the first send. Note that Resend's SPF and MX records go on the send subdomain and DKIM at resend._domainkey — a very common misplacement. Send yourself one confirmation and check it lands in the inbox, not spam, in Gmail and Outlook.
  3. Stripe live mode. Business details and payout bank account complete, or you can take money and not receive it. Live Payment Link created fresh, its "After payment" redirect re-configured (it does not copy from test mode), live webhook endpoint registered against your production URL.
  4. The payment wiring test, both modes. Test mode with card 4242 4242 4242 4242, then live mode with your own real card, then refund yourself. Confirm all three of: money in the Stripe dashboard, a row in checkouts, and the correct visitor_key. Do not launch on a test-mode-only pass.
  5. Vercel Pro. Required before the pre-order button goes live, per §3.
  6. Seed data wiped. npm run seed -- --scenario=clean, then confirm npm run status reads all zeros. Fixture rows in a real run are how you ship an idea nobody wanted.
  7. Start date recorded so the 10-day window and the readout's date range agree.

First 10 customers — concrete channels for this buyer. Tag every link with a distinct utm_source; an untagged channel is an unmeasured channel and is worth roughly nothing to you.

8. Extension paths


This is one of eight. The pack has seven more like it — subscription SaaS, metered API, paid Chrome extension, digital storefront, private knowledge base, price watcher, newsletter engine — same format, same gates. Get the pack for $39 →