Built in public, one release at a time
34 releases, each one live.
Each version shipped one change to the live site and the reason it mattered. 30 of them record the check that proved it worked. Newest first.
- v33Judges have about three minutes, and the strongest proof points were spread across four pages with nothing pointing at them.
Judges' tour at
/judge(and a Judges tab from tablet up): six steps, each with what to do, what to look for and a direct link (the refused rumour, the live guided deal, your own words or a WhatsApp export, asking the deal, outcomes with map, chase list and report, the changelog), plus a plain "what is real, what is illustrative" box covering the model and fallback, grounding, human confirmation, sample data, storage and rate limits.Verifiedall six links return 200 and the
#auditanchor exists; no overflow at 390 (tab hidden on phones); on the live model the sample's "Who has to sign off?" returns "The conversation does not say." and "What is the next deadline?" returns the exact grounded quote, as the page promises. - v32Choosing a country only filtered the list; an ecosystem lead still had to count and compare by hand to answer "how is this market doing?".
Corridor insight: picking a country (or corridor row) on the Outcomes map now shows what it means in numbers under the map, from confirmed stages only: deals touching it, the share that reached a confirmed meeting, and its furthest deal with a link.
Verifiedpicking Philippines shows "3 deals touching Philippines · 33% reached a confirmed meeting · Furthest: KlinikNow × Cebu Health Network · POC launched"; no overflow at 390.
- v31Ecosystem teams, a named audience, had no way to take the funnel to their sponsors or leadership.
One-page ecosystem report: a "Print report" button on Outcomes prints the funnel, conversion rates, chase list, deals and corridor table on white paper under a dated "HandshakeOS ecosystem report" header, dropping the WebGL map, data controls and every button except the corridor labels. The "deal brief" print label is now scoped to deal pages with
main:has(#actions).Verifiedin print media the dated header shows, map and controls are hidden, zero buttons render outside the corridor table, and the corridor names print (they had vanished because they are filter buttons).
- v30Returning users landed on sample tiles and had to open each deal to find out what they owed.
Returning visitors see what to do on every deal: once they have deals, Capture opens with "Your deals · what to do next", overdue deals first, each card showing its Next best move (v28 rules) and linking straight to that section of the deal.
Verifiedfresh browser shows no list; with two real deals and one overdue action, the overdue Nusantara deal is first with "Do it now: Draft pilot proposal" linking to
#actions, then Kedai Runcit with "Send the follow-up while it is warm" linking to#followup. - v29The guide's next action sat in a panel that scrolled away as soon as a visitor started reading the plan.
Docked guide: when the Prove-it panel scrolls out of view, its current step and single action ("Verify the reply", "Confirm: POC launched", "Send the rumour", "Reject it") dock as a bar at the bottom of the screen, with a link back to the guide; it hides again when the panel is visible. Picked from the D1 funnel: all 8 recorded plans came through the guided path, but only 4 sessions pressed the first guide button and 2 finished (small, mostly scripted sample).
Verifiedno dock at the top, dock appears after scrolling, three steps completed from the dock alone, deal reached "Verified: POC launched", dock hidden again at the top.
- v28The opportunity page laid out six cards of facts but never said what to do now, which is the step that converts a conversation into an outcome.
"Next best move" on every deal (when the guide is not running): one recommendation with its reason and a button that jumps to the right section, computed by rules from the deal's own data (
lib/nextMove.ts): evidence awaiting review, then overdue items (chase them, or do yours), then a committed promise needing proof, then "send the follow-up while it is warm". No model call, nothing invented.Verifiedfresh card deal gives "Send the follow-up while it is warm"; with the contact's action overdue it gives "Chase Dewi: Draft pilot proposal / due Tue, 20 Oct" and the button lands on the follow-up; with pending evidence it gives "Review the evidence waiting for you".
- v27A visitor with two real deals saw "Introduced: 21": their own metrics were buried in sample data.
Outcomes shows your own funnel: illustrative deals are on only until the visitor has captured a real one, then the funnel, rates and map default to "Your N deals only" with the sample set one toggle away; a scope line says exactly what is on screen, and rows are tagged YOURS or ILLUSTRATIVE.
Verifiedfresh browser shows the illustrative set (Introduced 19) with a CTA; after one real deal, "Your 1 deal only." with Introduced 1 and the toggle off; toggling on gives 20 and "plus the illustrative set".
- v26Founders forward a plan to a co-founder or bring it to the follow-up call; printing the dark app gave a wasteful page of controls.
Printable deal brief: a "Print brief" button on every opportunity and a print stylesheet that turns the dark page into a white one-deal brief (light palette, "HandshakeOS deal brief" header), keeps the funnel, actions, every quoted claim, risks, the follow-up message as text and the audit trail, and drops navigation, the guide, buttons, links, the Ask card and an empty evidence form.
Verifiedin print media no buttons or links render, the follow-up text prints, the empty Evidence Engine card and back link are gone; screen view unchanged.
- v25Tabbing through the app showed no visible focus and walked through the whole nav on every page.
Keyboard access across the app pages: a "Skip to content" link as the first Tab stop (jumps to
#main), and one teal focus ring on every link, button and field via:focus-visible, so keyboard users see where they are and mouse users see nothing extra.Verifiedfirst Tab reveals the skip link at the top, Enter moves focus to main, the next Tab lands on the first sample tile with a 2px
rgb(79, 209, 174)ring, and Enter on it starts the live analysis. - v24A typo in a shared link landed on Next's unstyled "This page could not be found", a dead end.
Branded 404 across both root layouts (
app/global-not-found.tsx,experimental.globalNotFound): "This link did not survive the audit." with the main CTA and links back into Capture, Outcomes and the changelog, real 404 status kept.Verifiedunknown URL returns 404 with the branded page at 1366 and 390 (no overflow), its "Run a real deal" CTA reaches the guided deal.
- v23A public AI demo with paid keys behind it shipped with no browser-side hardening at all.
Production security headers on every page: a Content Security Policy scoped to what the site really loads (self, Google Fonts,
blob:/data:media for the scrub video and image previews; model calls stay server-side),frame-ancestors 'none'plusX-Frame-Options: DENY,nosniff, a strict referrer policy and a permissions policy denying camera, microphone, location and payment.Verifiedheader served; walk of /, /ms, /capture, /dashboard, /versions and a business-card analysis logged zero CSP violations with the scrub clip, WebGL hero, Inter Tight and the corridor map all working.
- v22The plan page summarised a deal but could not answer the questions a founder actually asks before a follow-up call.
"Ask this deal" on every opportunity: ask a plain question and Gemma 4 answers only from that deal's own conversation and evidence, with up to three verbatim quotes, each grounding-checked; an answer with no quote found in the source is replaced by "The conversation does not say." New
/api/askships with the rate limit and size caps.Verified"What will Sarah send, and when?" answered with the exact grounded quote; "Who has to sign off?" and "What is their annual revenue?" both returned "The conversation does not say."; UI answered the business-card deal's deadline in 0.9 s.
- v21The public site spends a paid OpenRouter and Gemini key on every request with no limit; one script could drain the credit.
Cost protection for the paid model keys: Cloudflare Workers Rate Limiting caps model calls at 10 per minute per visitor IP across
/api/analyzeand/api/verify(lib/ratelimit.ts, bindingMODEL_LIMIT). Over the cap, built-in samples still answer from their recording (marked fallback) so a demo never dies; anything else gets a clear 429./api/verifygains the 20k-character cap analyze already had. Fails open if the limiter is unavailable.Verified12 rapid live calls from one IP on production, the excess refused with 429, a built-in sample still answered while limited.
- v20Someone watching the page or the demo had no zero-typing way to try the product in their own hands.
"Your turn" hand-off at the bottom of the landing (EN and BM): a numbered tutorial that walks one deal from promise to proof (open the sample, read the plan, verify Sarah's reply, reject the rumour), next to a QR card that opens the guided sample deal on the visitor's own phone. On phones the code is hidden and step 1 becomes "Tap Start the sample deal". QR is a static SVG (
public/landing/try.svg), no new dependency.VerifiedQR decoded from the rendered page on
/and/msequalshttps://handshakeos.m-naim.workers.dev/capture?sample=en&run=1(200 live); 390px phone run reached/opportunity/...?guide=1in 12.1 s; no overflow at 390; typecheck green. - v19Judges evaluate a hackathon build; the iteration and its evidence were invisible to them.
Public changelog at
/versions(and a Versions tab): every release rendered fromdocs/versions.mdat build time, newest first, with what changed, why, and the recorded check split out as "Verified". The headline counts only what the log supports. Tabs tightened so the three-tab nav fits a 390px phone.Verifiedpage prerendered static, counts match the log (19 releases, 15 with a recorded check at build), no overflow at 390 on /versions, /capture, /dashboard.
- v18A slow OpenRouter call made one analysis take 63.5 s, because Gemini only started after the 60 s timeout.
Hedged model calls: Gemma 4 via OpenRouter stays primary; if it has not answered in 15 s (or fails sooner) Gemini starts in parallel and the first good answer wins, the other call is aborted. Per-call timeout 60 s to 45 s. Race logic in
lib/hedge.ts.Verifiedsix-case Node check (fast primary, slow primary/backup wins, early failure, slow primary still wins, both fail, no backup); three live analyses still served by OpenRouter Gemma 4 in 5.1 to 8.1 s.
- v17Clicking from the cinematic dark landing into a beige spreadsheet made it look like two products.
One visual identity: the app pages (Capture, Opportunity, Outcomes) take the landing's dark palette through the theme tokens, plus a new
accent-inktoken for text on teal,color-scheme: darkfor native controls, and a raised teal surface for the Prove-it guide.Verifiedscreenshots of /capture, /dashboard and a guided opportunity at 1366x900, no hard-coded light colours left in app components.
- v16On phones the hero showed blurry skyline while the story's anchor (last night's card) hid under the copy.
Phone hero art direction: a portrait cut of the Grok dolly (
hero-dolly-portrait.mp4, 540x960, cropped at x=150 so the card and phone stay in frame for the whole push-in), copy moved over the sky with a top scrim, landscape poster positioned to match. The three.js card field is off below 860px (not even initialised). Oldhero-dolly-m.mp4and dead.chip--b/ mobile.hero__netrules removed.Verifiedat 390x844 the card and phone sit fully below the copy at top and mid-scroll, WebGL not initialised on phone, still initialised on desktop, no overflow.
- v15ASEAN deals live in WhatsApp threads; getting one in meant hand-copying message by message.
WhatsApp chat import on Capture: "Import WhatsApp chat (.txt)" button plus drag-and-drop onto the conversation box.
lib/whatsapp.tsparses Android and iOS exports, joins multi-line messages, drops system lines, media placeholders and deleted messages, and keeps the most recent messages within 18k chars; non-chat files fall back to plain text.VerifiedNode check on Android and iOS fixtures (noise dropped, multi-line kept, non-chat returns null, truncation keeps the newest); live Gemma turned an exported chat into "Bayu Solar Invoicing Pilot" with Arif Rahman as contact.
- v14Startup ASEAN in Malaysia, a product that claims "any ASEAN language", and a landing that only spoke English.
Bahasa Melayu landing at
/ms(own static page with hreflang alternates and ms_MY Open Graph), EN/BM toggle in the bar on both. All landing copy moved toapp/(marketing)/copy.ts; the audited source messages stay in their original language because they are the evidence.landing_viewnow records the language.Verifiedtoggle navigates EN to BM,
document.langisms, the audit peak reads in BM, no overflow at 390. - v13Outcome conversion leaks where promises go unchased; the dashboard counted deals but never said who owes what this week.
"Chase this week" on Outcomes: for each of your own deals, promises not yet delivered (committed evidence beyond the current stage) and open actions overdue or due within 7 days, soonest first. Items the other side owes get a one-click nudge (copy or WhatsApp) addressed to the contact by first name; your own items are marked "Your move". Illustrative seed deals never appear.
Verifiedempty state on a fresh browser; after the English sample, Sarah's requirements (Sat 7 Nov) show "They owe this" with a WhatsApp nudge, the scope task (Thu 5 Nov) shows "Your move".
- v12Every link to the site (submission, showcase posts, WhatsApp) previewed as bare text.
Social share card: a 1200x630 card rendered from real HTML (KL plate, headline, a COMPLETED quote next to a struck-through HEARSAY one) via Playwright, so the type is crisp, not generated. Open Graph and Twitter
summary_large_imagemetadata on both root layouts withmetadataBase.Verifiedog:image resolves to an absolute URL and returns 200 on the live site.
- v11With live Gemma, visitors who arrive with their own conversation had to leave, find the box and paste again.
Own-notes box in the landing close: paste a real conversation and "Audit my notes" sends it straight into the live audit. Text travels through sessionStorage (never the URL, cleared on read), enabled from 20 characters.
Verifiedtyped notes became "Bayu Solar Invoicing Pilot", text absent from URL, handoff cleared; close stage fits at 390x844.
- v10The landing asked for trust with words only; it now shows what has actually happened on the site.
Live proof counters on the landing (hero and close): deals audited by Gemma 4, steps verified, rumours refused, read from the D1 event log through an aggregate-only
/api/stats(60 s cache). Hidden when D1 is empty or unreachable, so the page never shows an invented number. Backlog v10 to v33 written into the plan doc. - v9With real Gemma the analysis takes 9 to 15 s, and the only progress UI rendered below the fold: a tile click looked like nothing happened.
Live audit overlay on Capture: the moment a sample or own notes are sent, a full-screen panel shows the visitor's own conversation (and the card photo) with a reading sweep, the real model id and a real elapsed clock. When Gemma answers, the quotes it cited light up inside their text ("6 claims found, every one quoted from your words") before the plan opens. Old below-the-fold step list removed.
Verifiedoverlay visible 158 ms after click, 6 real citations marked, guide opens; overlay covers the full viewport at 1366x768 and 390x844.
- v8Every version so far was judged by scripted runs. Now each one can be judged on what real visitors do.
Real conversion tracking: anonymous funnel events (landing_view, audit_peak, cta_click, capture_view, sample_run, own_analyse, plan_created, guide_verify, guide_confirm, guide_done, dashboard_view) go to D1
handshakeos-eventsthrough/api/track(allow-listed names, random per-tab id, no IP or personal data, 300-byte meta cap). Schema inmigrations/0001_events.sql. From v8 on, builds ship from a clean checkout of the tag, never the shared worktree.Verifiedvalid event 204, unknown name / bad id / non-JSON 400; live rows counted after a scripted visit.
- v7Judges were watching a recording. The badge said Demo mode because no model key was set.
The public app calls open-weight Gemma 4 through OpenRouter (
google/gemma-4-26b-a4b-it, paid route). The header says Gemma 4 live. Each result names the model, the provider, and the seconds. Own notes work. The scripted audit is a real call, with the canned sample kept only if that call throws. - v6"Paste your own notes" led to a box that errored only after the visitor typed and clicked; the card sample errored outright.
Capture page leads with three one-click conversations shown as the real artifacts (English notes, a Bahasa Melayu WhatsApp bubble, the business-card photo); the English one opens the guided Prove-it flow. "Paste your own" now says up front that the public demo has no Gemini key. Fixed a pre-existing bug: the business-card sample could never run in Demo mode (analyze route skipped canned output whenever an image was attached and no card result existed); added a recorded card analysis whose transcript is the card's exact text, so every quote stays grounded.
Verifiedeach tile reaches a finished plan in one click (215 to 340 ms), 0 ungrounded quotes, no overflow at 390.
- v5The hero was a still photo with a parallax; the first scroll now moves the visitor into the scene.
Hero becomes a scroll-scrubbed Grok image-to-video shot (from the v1 plate): the camera dollies in toward last night's business card while the three.js noise cards fall away. Encoded for scrubbing (720p GOP 8, mobile 480p GOP 4, audio stripped); plate stays as poster, reduced motion never fetches the clip.
Verifiedplayhead tracks scroll on desktop and 390px (0, 1.1, 1.9 s at p 0, 0.39, 0.65), clip visible after first seek, no page errors.
- v4The ecosystem view, the one thing Startup ASEAN cares about, was a plain table under a spreadsheet.
Outcomes page gets a three.js ASEAN corridor map: real capital coordinates, an arc per corridor coloured by its furthest confirmed stage, particles only where a deal moved past Introduced, rings for domestic deals. City labels are real buttons that filter the opportunity list; corridor table rows filter too. Fixed a pre-existing mobile overflow (dashboard was 420px wide on a 390px phone) by giving the grid
minmax(0,…)columns.VerifiedPhilippines filter 20 to 3, all Philippine corridors; no horizontal overflow at 390 and 1366 on /, /capture, /dashboard.
- v3The hero said "stop collecting contacts" in words only; now the first screen shows it.
three.js hero layer: ~150 business cards (drawn card faces, instanced) float over the KL plate; six verified ones light teal and thread to you, the rest drift off and fade as the hero scrolls. Pointer steers the rig. Loaded on demand, paused off screen, one still frame under reduced motion, confined to the left 60% so copy stays clean.
Verified60 fps headless at 1440x900, copy unobstructed, reduced-motion frame renders.
- v2v1 landed visitors on six dense cards with the Evidence Engine buried: the aha the landing promised needed 4+ unguided clicks.
"Prove it" guide on the opportunity page for visitors from the landing CTA: verify Sarah's reply, confirm it, then try to fool the engine with a rumour and watch it get refused. verify/decide extracted into shared functions.
Verifiedlanding to verified POC in 3 clicks, 1.0 s (Playwright).
- v1The old home opened on an empty textarea with a disabled button: the visitor had to understand the product before seeing it.
/is now a scroll-driven story (scroll-craft engine): layered Grok hero, a live audit of a four-message deal as the peak, a light "three levels" act, a close with one CTA. The tool moved to/capture, which accepts?sample=en|ms|card&run=1so the CTA lands on a finished action plan in one click. Favicon added. - v0Baseline.
Served from Cloudflare Workers via OpenNext. No product change.