Week of Jul 21 → Jul 27. Receipts, not lessons. Four things went live on production, one of them fixed a bug that was quietly costing us money, and one closed a loop that had been open since the affiliate programme started. Every item cites the tag or version it shipped under.
The affiliate money path was leaking attribution and, on some requests, leaking clicks entirely — both are fixed and verified live. Half of every traced NordVPN dollar could not be tied to the page that earned it, popup revenue was being credited for gateway sales, and any visitor whose browser carried a cookie with a stray % character got a dead redirect instead of a purchase. And by the end of the week the loop was closed: Nord's conversion postback is wired up and verified, so a purchase now traces back to the exact click that produced it. Alongside it: the rich FAQ system went site-wide across ~126 pages in 16 languages, domain-broker v2 went permanent on /domain-broker/, and the get-started paid lander is now running its own A/B.
Five deploys on vpn-out (073ea3a7 → 0d9546b5 → 1e024a2e → b79af5b5 → aa87c22e), one on vpn-translate (d2299217), 11 partner configs re-slotted in PARTNERS_KV, and two D1 migrations.
1. The gateway was throwing away the content page. Our /out interstitial is served at /out, so when a visitor clicked its buy button the referrer was /out and the real page was lost. 371 NordVPN clicks in July reached the affiliate labelled _out — meaning half of all traced revenue was unattributable to any page. All 12 gateway CTAs now carry the origin page explicitly.
2. Popup revenue was being over-credited. The popup slot took whatever A/B cookie was in the browser, and those cookies live 30 days. So a visitor who saw a popup once and bought through the gateway a week later reported as a popup conversion. Corrected read: the gateway is the top earner — $900 of July's $1,257 (72%). True popup revenue was $270 (21%). Worse, the domain-broker hero A/B — a lead form test — was leaking its variant onto 43 affiliate URLs.
3. A live 500 on the money path. /out parsed every cookie in the jar unguarded, so one bare % in any cookie — a consent or analytics cookie we never even read — threw an error and killed the redirect. Confirmed live before the fix: production returned 500, the patched build returned 302. Duration unknown; it was silently losing sales.
4. Landing page now exists. It didn't, anywhere. /out only sees the click, never the arrival, so vpn-translate (routed on every page) now stamps the session's entry page into a cookie that /out reads at click time.
5. One schema across every partner. Slots previously meant different things per partner — aff_sub2 was the page for NordVPN and the placement for Proton, so any combined report was silently wrong.
6. And the loop is now closed on Nord. With the schema standardised, a per-click ID could finally ride the link and come back on the sale. We can trace a purchase to the click that made it — see the postback item below.
| Slot | Now carries |
|---|---|
| sub1 | referring page (where the click happened) |
| sub2 | landing page (where the visit started) |
| sub3 | exit-intent popup, when applicable |
| sub4 | use case |
| sub5 | placement — reclaimed from a hardcoded constant sent on every click ever made |
Because we were optimising blind and didn't know it. The July affiliate report looked healthy — until the biggest single row turned out to be "we don't know where this came from," and the second-biggest was crediting the wrong thing. Every decision downstream of that report (which pages to build, which placements to push, which providers to negotiate with) was resting on it.
The 500 is the sharper point: it wasn't a reporting problem, it was lost revenue, and nothing would ever have surfaced it. It doesn't show up in conversion rate, it doesn't trip an error budget, and the visitor just sees a broken link and leaves.
page vs landing_page are separate columns, so we can finally distinguish what pulled a visitor in from what closed them. Those have always been different questions and we could only answer one./out (i18n, attribution, postback, resilience) plus a landing-cookie gate on vpn-translate — all mutation-tested, so they actually fail when the protection they guard is removed.Technical honesty for the room: two of the bugs above were found while fixing the first one, and one near-miss was mine — an early revision would have silently dropped every homepage click (~509/month) because / normalises to an empty path. A test caught it; no build gate would have. That is the argument for the gates, not for the fix.
(Notes #193, #195–#199. Commits f7a5b65→e8f5e2e in vpn-workers — six, still unpushed.)
NordVPN conversion postback — wired up and verified end to end (vpn-out aa87c22e, migration 0005)
We can now answer the question the affiliate dashboard cannot: which click actually bought. /out mints a per-click UUID, sends it to Nord, Nord returns it on the sale, and a join resolves the payout to the exact click — and through it the page, entry page, use-case, placement and country already on that row.
Verified live with a real postback:
transaction MANUAL-1 · nordpass · $5.00
clicked from /vpn/nordvpn/ landed on /vpn/
use case streaming placement qa_pb_prod
country US click → sale: 17 min
vpn.com/out* already covered /out/postback, and vpn-out already binds the clicks database. One endpoint, one table, one column.offer_id, not from the dashboard. The postbacks were pasted without a &partner= param, and one Nord account serves NordVPN, NordPass, NordPass Business, NordLayer, NordProtect and Incogni. Defaulting them to nordvpn would have kept the revenue total correct while silently corrupting the per-product split — the kind of error nobody catches. offer_id=488 now resolves to nordpass on its own; an unmapped offer resolves to NULL and logs, rather than guessing.INSERT OR IGNORE on a UNIQUE transaction_id, and a synthesised dedupe key when a postback arrives without one, so a replay can never multiply revenue.UA-90742505, retired 2023), one at anytrack.io, one at a Cloud Run container.⚠️ Read the coverage honestly — it ramps, it doesn't switch on. Only 3 of 25,378 clicks are joinable today (those minted after the deploy), and there are no real conversions yet. Nord's attribution window is ~30 days, so sales arriving over the next few weeks will often reference clicks made before this shipped: those postbacks land with an empty click_uid and won't join. That is expected, not a bug — worth knowing before someone opens the table next week and reports one. Full click-level coverage arrives as the pre-deploy click population ages out.
Available right now with no engineering: Nord's own sub-ID report can already break conversions down by page, retroactively — we've been sending the page in a sub-ID slot for weeks. That's dimension-level rather than click-level, but for deciding which pages to build it is most of the value, and nobody has pulled it.
FAQ system — site-wide, English + 16 languages (tag prod-2026-07-24-001)
scripts/lib/provider-facts.mjs, which killed a set of cross-page contradictions (ProtonVPN price, ExpressVPN devices, Nord fleet counts, CyberGhost rank #3 → #10).Domain broker v2 — promoted to 100% on /domain-broker/ (tag prod-2026-07-22-001)
DomainBrokerFormV2 now serves everyone, permanent and indexed. H1 "Premium Domain Broker" and self-canonical unchanged, so ranking is unaffected.a-cookied visitors see v2 but their leads still tag hero_variant='a'. Use pre-promote data only for any retro./{lang}/domain-broker/ hubs deliberately stay on v1 — FormV2 isn't i18n-ready yet.V1 vs V2 — what the data actually supports. Two findings survive scrutiny:
Domain-broker form V2 more than doubled on-page engagement and doubled median domains submitted per seller. Average time on
/domain-broker/rose from 115s (Jul 1–12, 380 sessions, V1 only) to 265s and 475s in two independently measured V2 windows (Jul 23–24, 54 sessions; Jul 13–24, 321 sessions). Median domains per seller rose from 1 (n=46) to 2 (n=14).
Source: askbowtie page metrics + D1 domain_intake / domain_intake_items, measured 2026-07-24. V2 is identified by the presence of an e-signature (agreement), the only field FormV2 can set and V1 cannot. The engagement figures are keyed on page and date, so they don't depend on inferring a form version at all — that is why they're the sturdiest numbers here.
"So did it make more money?" — not knowable yet, and this belongs in the same breath as the numbers above. CRM qualification lags submission by 20–25 days and V2 launched Jul 13, so the first V2 qualifications aren't due until early August. Zero of the V2 leads have qualified, and that is expected timing, not a bad signal. Do not let an engagement number be read as revenue.
Excluded as unreliable — do not quote these:
budget is a self-reported band (10k/50k/100k/250k/500k/1M), not verified value. On sell leads it's an asking price, which isn't the same quantity as a buy budget.DomainBrokerForm.astro:92 has an add-domain button), so this is a change in seller behaviour, not a capability V2 unlocked.Get-started paid lander — A/B live (prod-2026-07-22-002, GETSTARTED_AB_ACTIVE=true)
domain-broker-get-started-2026 on /domain-broker/get-started/: a = the legacy flat form, b = DomainBrokerFormV2. 50/50, sticky 30-day cookie, bots to control.serveAB(cfg) now drives both tests, each stamping its own test id so results can't cross-merge.gs_variant, which is attributed server-side from the source path plus the cookie and never defaulted. The itbroke ab_variant events are the exposure denominator only — do not chain browser to server through a bowtie flow.ab_domain-broker-get-started-2026=b → path gate passes → gs_variant='b'. The Cookie header is threaded server-side at forms/src/index.js:172. The mechanism works; no fix needed.?ref=popup-broker-2). At ~2 leads/day split two ways, this test needs weeks, not days — and there is precedent for calling that dead: the provider-plan A/B was abandoned at ~5–10 conversions/week for exactly this reason.1. Confirm ExpressVPN's Impact sub-ID parameter names. We're sending the documented Impact standard (subId1–subId3). If it's wrong the click still pays, but it would look like working attribution while reporting nothing — the worst failure mode. One look at the Impact dashboard settles it.
2. Windscribe and Mullvad earn us $0 on ~895 clicks/month. Neither has an affiliate deal; /out sends them to the provider's own homepage. Either negotiate, or stop routing buying intent to them for free. Decision needed — this is commercial, not technical.
3. Verify PIA and CyberGhost sharing one affiliate ID. Both configs point at the same base URL with aff_id=1312, differing only by offer_id. Both are Kape brands so a shared platform is plausible and probably correct. If it isn't, PIA's commissions are landing in CyberGhost's account. 30 seconds to check.
4. Configure postbacks for the other networks — Proton first. ✅ Nord is done (see above). But Proton is 2,531 clicks/30d, effectively tied with Nord's 2,803, and it is completely dark — same for Surfshark (451), CyberGhost (480) and PIA (405). Their clicks already carry the join key and their KV slots are mapped; each just needs one entry in its own affiliate dashboard, same URL pattern. Until then we have zero visibility into whether half our outbound volume converts at all. Decision: who has dashboard access for Proton?
5. Get-started A/B — keep it running, or call it? At ~2 leads/day split 50/50 it needs weeks to say anything, and get-started volume dropped to 1 then 0 leads/day right at activation while the hub held steady. Decision: re-measure Jul 27 and kill it if the dip holds — a test that suppresses conversions while producing no verdict is worse than no test.
6. Five new languages — Hebrew, Russian, Thai, Norwegian, Polish. Infrastructure ready; the rollout is a multi-hour bulk translation job needing a green light. (Carried from Jul 21 and Jul 14 — unchanged.)
7. vpn-workers commits are local-only. Four commits from this week's attribution work are unpushed.
| Metric | Value | Read |
|---|---|---|
| Sessions | 6,518 | +10% wk/wk, +11% vs 4-wk avg |
| Conversions | 1,079 @ 16.6% | +73% wk/wk — but see the caveat below before quoting this |
/out 5xx | fixed | The malformed-cookie 500 is gone; expect the error rate to fall by design |
| Incidents | 1 critical (3 sessions) | Rest is low ad-block noise; two network-error clusters worth a look |
⚠️ Do not quote the conversion jump as growth. Three measurement changes land inside this window: the dashboard conversions chart was fixed on Jul 23 after under-counting ~94% of conversions, conversions moved server-side on Jul 16, and the sub-ID slots changed meaning on Jul 24. Read conversion levels, not week-over-week deltas, across this period. Also filter placement LIKE 'qa_%' — QA clicks from the attribution work are in Friday's data.