Skip to content

Site: sell what the product does, capture founding-price interest, and publish the security page - #75

Merged
bharathm03 merged 15 commits into
developmentfrom
feat/site-copy-and-agent-compat
Sep 2, 2026
Merged

Site: sell what the product does, capture founding-price interest, and publish the security page#75
bharathm03 merged 15 commits into
developmentfrom
feat/site-copy-and-agent-compat

Conversation

@bharathm03

Copy link
Copy Markdown
Contributor

Reworks the marketing site around what the product actually does today, opens a founding-price capture where three dead paid buttons used to be, and publishes a security page a reader can check against the repo.

Deploy order is load-bearing

web first, then site and app. Two independent reasons:

  1. This branch adds a migration (20260901000000_add_waitlist_signup) and the endpoint that writes to it. The site's pricing cards POST to WEB_URL/api/waitlist cross-origin — ship the site first and every submit hits a 404.
  2. Production CORS_ORIGINS must list the marketing site's origin, not just the app's. Nothing in either test suite can catch this: it fails only in a real browser, as a network error, with nothing in the web service's logs to say why. Recorded in DEVELOPMENT.md in this branch.

What changed

The paid path. All three surfaces that showed an unbuyable button now capture an address instead: the site's plan cards, web's /pricing, and the device-cap dialog (which links out to the site's capture rather than capturing in-app). app/lib/screens/upgrade_screen.dart is the one CTA left dead — see Open below.

Positioning. The home page moves from "remote control" to a control plane; the hero takes back the overview claim from Fleet.astro, because the evidence gate is opt-in and on Pro, so "make it prove it" was false on every free machine until someone armed it. The one promise a stranger is asked to believe now holds on a bare install. Features points at #handler rather than scrolling readers past the only thing anyone pays for. A new Compat.astro answers "does this work with my agent" before the gate is sold.

A security page, with every claim linked to the code that implements it.

Claims corrected to match the code. A team-fleet line with no code path (mayRoute is same-uid and it is the only routing authorization the relay has); Handler's headless tier described as read-only when opencode is transcript; three sign-in options where the code has four; a README that called CAPABILITIES the whole gate when the machine count is a second paywall; a privacy policy that never disclosed the waitlist row this branch starts writing; and a hero pill quoting a percentage off a price that has never been charged — the invented reference price PlanCard's own comment refuses to print.

Atmosphere and build. The hero's glow was authored in fixed pixels against a ~1440 canvas, so it was a constant 434px circle at every viewport — a light on desktop, a flat brown tint with a hard edge on a phone. Below md it is a top-edge wash instead. And icon() was inlining all ~3,700 simple-icons into the build to draw seven chips.

Gates

  • site: astro check 0 errors / 0 warnings / 0 hints (44 files); Playwright 90 passed (desktop + mobile)
  • web: tsc --noEmit clean; 627 pass / 0 fail
  • app: analyze_files clean on the touched file

New coverage worth naming: the og:image the meta tag names must exist in the build (the filename tracks the card's claim, so every recut edits a string nothing checked — get it wrong and the card 404s on every page at once while the suite stays green); the agent roster is pinned in #agents, whose accent must stay in lockstep with handlerObservable; and waitlist.test.ts covers the endpoint.

Open, deliberately

  • upgrade_screen.dart is the last dead CTA. The cap dialog already answers "this can't be bought yet" by linking out to the site — applying that pattern here is small and precedented, but the block is inside the TEMP-PROMO scaffolding that gets deleted when checkout opens, and a browsing reader is a different intent from someone who just hit a wall. Left as a call to make.
  • The Pro card still sells "Shared fleet view across the team", and the FAQ still says a seat "sees the team's fleet". Same false claim removed from Fleet.astro here — but deleting a sold feature off a pricing card is a pricing decision, not a review fix.
  • No product screenshots anywhere; no JSON-LD; no llms.txt.

Terms section 1 said local use is free and paid plans add remote control, which contradicts section 5 and the free plan the whole funnel rests on. The wrong version came first, in the operative agreement.

The privacy policy described the relay briefly buffering messages for offline recipients. It does not: server.ts refuses the frame with PEER_OFFLINE and drops it. The uniform refusal is deliberate — an unauthorized sender must not learn which devices are online — so the policy was describing a weaker relay than the one we ship.

Support named Gemini CLI, which is not in the agent registry at all; Cursor is. SSO, audit log and IP allowlist were claimed as shipped on the marketing pricing page, in web's plan UI and in support: the capability flags exist on the plan model but nothing reads them, so all three now say roadmap and cross-reference each other.

Enterprise leads went to /support, which put a budget holder on the troubleshooting page.
The headline spent the largest type on the overview ("One screen."), which is a watching claim for a product that acts, while the argument the ProofCard beneath it makes went unstated in anything above body copy. The kicker now carries scope and the search terms; the headline carries the wedge. Fleet still owns the overview claim, being the section that proves it.

Leading with the gate needs the roster to be honest about who gets it. The new band under the hero lists every supported agent and reserves signal for the three handlerObservable answers true for — a terminal session needs the integration to POST /handler-event, a chat session needs a driver. In this palette the accent means the system is doing something (see .live-cells, and why Eyebrow gave the colour up), so ten glowing marks would promise the paid feature to six agents that cannot run it. The sentence below names the three in words as well: colour reinforces, it never carries alone.

The roster left CrossAgent because section seven is too late to answer a question that decides whether someone keeps scrolling.

Typography: the kicker's first sentence needed 783px in a 672px box and stranded a two-word tail above a display headline; the availability line was four clauses in a 21rem column and split "iOS &" from "Android". Both are now one sentence per line.

Mobile availability read as "you can't have it yet" while TestFlight and Play internal testing invites were in fact open.
The repo is public, SECURITY.md exists, the handshake spec is written down and the relay client is Apache-2.0 and auditable. None of that was reachable from the site, so the strongest evidence we have was also the least visible.

The page claims architecture only: what the crypto is, what has to be true before a phone can drive a machine, and — the section that does the most work — exactly what metadata the relay does see. Every claim that has a limit prints the limit beside it, including the ones that are unflattering. No SOC 2, no DPA, no residency, no compliance language of any kind, because none of it is true today and a reader can check.

Adds .well-known/security.txt per RFC 9116, and the text/plain MIME type Azure needs to serve it: the global X-Content-Type-Options: nosniff means a wrong content type would have been fatal rather than cosmetic.

home.spec.ts asserted nothing links to /security. It now asserts the opposite.
…ree surfaces

Every paid CTA rendered as a disabled "Available after beta" control, and the site had no email capture anywhere. That is the one instrument that produces pricing signal before launch, so the shutters become a form: POST /api/waitlist on the web service, backed by a Prisma model, Zod-validated, CORS'd for the marketing origin, and idempotent on a repeat address so it never leaks whether one is already on the list.

The capture names no figure. An address is not consent to a price, and we have no pricing data yet to set one with.

The card hosting it no longer strikes through $99. That price has never been charged, so rendering it as a crossed-out former price invents a reference price the product never had — the thing CCPA's dark-pattern rules and EU Omnibus Art. 6a both reach. Stated forwards as the list price at launch it is the same contrast and a true sentence. pricing.spec.ts now asserts the absence of any strike-through as well as the figures.

The app's worker-cap dialog stops offering an Upgrade button that could not be pressed and points at the same list; it is copy and a link, so it degrades to a dead link rather than a broken flow if web deploys later.
… the wrong category

Nav and footer sent Features to #fleet. Phases.astro (#handler) sits above Fleet.astro, so the one link a reader clicks to find out what the product does opened one section PAST the only thing anyone pays for. It is now #handler; scrolling on from there still reaches the fleet view.

The home title said "remote dev for concurrent coding agents". "Remote dev" names the cloud-workspace category — Codespaces, Gitpod, devcontainers — which is the opposite of what this is: the agents run on hardware the reader already owns. It now says "remote control for Claude Code, Codex and Cursor", which is accurate, names the agents people actually search for, and fits in 58 characters. The description drops "sequences the work and proves it's done" for the hero's own sentence, so the result and the page it opens agree.

404.astro answers every unknown path, so a mistyped inbound link could be indexed under its own URL as a page saying nothing exists. Seo.astro grows a robots prop, set only there; every other page still emits no robots tag at all.

Three contracts, because none of this could fail loudly. Anchor hrefs are excluded from home.spec.ts's dead-link sweep (a fragment never reaches the server), so a renamed section id is the one link on the site that rots silently — all of them are now resolved against the DOM. Features is pinned by target. And the noindex is asserted on /404 together with its absence on /, since a stray default there delists the site.
Remote control is 1:1 and commoditising — every agent vendor is shipping phone access, and no company buys "reach my laptop". A control plane is 1:many: N machines by M agents on one screen, which is the version of this a team recognises as its own problem. Remote development was the other candidate and is worse: it is the Codespaces/Gitpod shelf, judged on workspace provisioning and browser IDEs, and it centres a human at a keyboard, which is what we are positioning away from.

The headline takes the overview claim back from Fleet.astro. It was handed over when the h1 carried the evidence gate, and the gate was never entitled to it: Handler is opt-in and it is on Pro (CAPABILITIES in bridge/src/entitlement.ts), so "Make it prove it" was false on every free machine until someone armed it. The one promise a stranger is asked to believe now holds on a bare install. Fleet keeps the proof, and the ProofCard under the hero still shows the gate working.

The gate drops to the second beat rather than out: it is the only thing here anyone pays for and the only part that is hard to copy, so the hero points at it with a verb — you ARM Handler — and Phases.astro keeps the whole argument. Fleet's lede stops restating the hero and picks up the team scale instead; the footer follows the same order.

The og card needed no re-cut. It already says "Every agent. Every machine. One screen.", which disagreed with the old headline and agrees with this one.
Three questions decide whether someone stays: what is this, may I use it, may I contribute. The first was answered in prose and the other two were 100 lines down, so a reader who cared about the licence found out about ELv2 and the closed PR queue only after investing in the page. All three are now above the fold in one NOTE callout.

The tagline follows the site to the control-plane position. The supervisor keeps its place as the differentiator — a README has room for both, unlike a hero — but it is now labelled as the paid tier and cites the gate (CAPABILITIES in bridge/src/entitlement.ts). Someone who builds from source and finds the supervisor inert should learn why from us, not from an issue thread.

The security section gains what it was missing: the limits. No external penetration test, no certification, and the trust boundary is emptied of the relay but not of the account service — app.antgrid.ai serves the device inventory your phone reads a machine's Ed25519 identity from, so it is trusted to hand you the right key even though the relay never is.

A CI badge, since the workflow is public and a green one is cheap evidence for a pre-release repo. antgrid.ai/security is deliberately NOT linked: it 404s until the site deploys, and the in-repo spec and sources it would point at are already linked here.
The glow was a fixed 980x620 box with a closest-side gradient, so its visible
circle was 434px across at every viewport. On a desktop that is roughly a third
of the frame with dark air either side, and the falloff is what makes it read as
a light source. On a phone it is wider than the screen: no falloff lands in
frame, so the light flattens into a brown tint over the kicker with one hard
horizontal terminus and no shape to it.

Below md it stops being an orb and becomes an edge — the gradient's centre sits
on the top edge, so only its lower half is ever visible and the falloff runs down
the one axis a phone has room for. Alpha drops with it, because 0.16 across the
whole of a small frame is a far larger event than 0.16 across a third of a large
one, and it sits behind the lowest-contrast text on the page. Above md the halo
is unchanged in look but sized min(980px, 68vw), which also fixes 768-1024 where
the fixed box overflowed the same way.

The live cells go with it. They are placed in raw px so they land on the 32px
background pitch, which means on a phone only the leftmost column is on screen
and all of it sits behind the copy rather than beside it; a warm block fading in
and out under muted body text reads as a rendering fault. Liveness is already
carried in that viewport by the beta pill and the ProofCard's loop. The one cell
at x=96 moves to the right gutter for the same reason a viewport wider: the
shell is 72rem, so its left margin shrinks with the frame and by 1280 that cell
was sitting on the kicker.

astro check 0/0/0, 86/86 Playwright. Verified at 320, 360, 390, 430, 768 and
1440; the closing CTA's glow is left alone, its card frame contains it.
Success was decided on res.ok alone. Any 2xx from something that is not this endpoint -- a maintenance interstitial, an SSO landing page, a CDN error page served 200 -- hid the form and told the reader they were on the list. No row was written and no retry was possible, because the form was gone. Both clients now require the body's `ok` as well as the status.

web's submit button shipped enabled while the form's action names a JSON-only endpoint, so a page whose script failed to load fired a native urlencoded POST and navigated the reader off /pricing onto a raw error body. It ships disabled now and the script enabling it is what says the handler is attached; a noscript note gives the scriptless reader the email route. The site's card already worked this way.

web's card bound its controls by fixed id -- and its own comment claimed a second copy would bind its own. It would not: duplicate ids, and the second card's label focusing the first card's input. Bindings are per-form data attributes now, ids are per instance, and the status paragraph carries one so aria-describedby can reach it.

Disabling the control a reader just activated blurs it, and focus fell to <body>: their next Tab restarted at the top of the document, permanently on the success path since the button stays disabled there. Focus is reclaimed only when it did in fact land on <body>, so someone who tabbed on keeps their place.

WAITLIST_SOURCE was "pricing" on both surfaces. email is UNIQUE and the insert is ON CONFLICT DO NOTHING, so the app's logged-in card and the public marketing page were indistinguishable in the one column that exists to tell them apart -- and unrecoverable after the fact, on the release whose whole point is measuring founding-price demand. web sends "app_pricing".

The 400 echoed Zod's issues from an anonymous, cross-origin-allowed writer that neither client reads them from -- both pick their wording from the status code. Dropped. The 254 bound now applies to the trimmed value; the outer bound only stops an unbounded string reaching toLowerCase.

Site: WaitlistSource is a closed union, so a surface added with a space or a capital fails astro check instead of 400ing at every reader with copy that blames their email address. The status line reserves two lines -- every message there wraps at the card's mobile width. And cors() gets maxAge, since the fetch default caches a preflight for 5s and every retry paid a second round trip.
Fleet's "On a team, every seat's machines land in this same list" has no code path. mayRoute is the only routing authorization the relay has and it is same-uid; devices are listed per userId, and each seat signs in as itself. The security page shipped in the same release says the opposite in as many words.

The security page called Handler's headless runs "read-only tools over that working tree". That is the readonly tier. opencode is the transcript tier, and registry.ts says why in as many words -- config-level rather than flag-proven -- with judge.ts compensating by withholding the transcript path. opencode is one of the three agents this same release advertises as Handler-supervised, on a page whose entire argument is that it states every limit. "It never borrows a different vendor's agent" also ignored a user-set judgeTool.

support.md offered three sign-in options. better-auth enables emailAndPassword, the app has a real password step, and the security page added in this same release correctly names four.

The README said CAPABILITIES is the whole gate and everything above it is free. The machine count is a second paywall (FREE_WORKER_LIMIT, the only one enforced on a server), and this same branch replaced that dialog's Upgrade button with a waitlist -- so a second machine is not merely refused, it is currently unpurchasable.

privacy.md never disclosed the waitlist row this release starts writing: no collection entry, no retention period, and a deletion path that requires a registered account a waitlist signer does not have. All three added.

The hero pill quoted a percentage off $99, a price that has never been charged -- the exact invented reference price PlanCard's own comment refuses to print, and for the same stated reason. It quotes both figures forwards now. The FAQ's founding/list framing is gated on OFFER_ACTIVE like every other offer string, so flipping the documented switch no longer leaves the card and the FAQ contradicting each other on one screen.

Compat's prose is derived from the array the chips are built from, so it cannot go on naming three while a fourth chip lights up -- the accent has to stay in lockstep with handlerObservable. home.spec's cross-agent test was asserting the roster at page scope after it moved to Compat.astro, so it stayed green while checking a different section than its name; it is scoped now, and a new test pins the supervised three and the catch-all in #agents.

Docs: relay-requirements' archived banner now covers the offline queue that never shipped and that /privacy denies exists, and DEVELOPMENT records that production CORS_ORIGINS must list the marketing origin -- nothing can catch that mismatch but a reader.
icon() was registered with no include, and astro-icon assigns an installed collection ["*"] -- so adding @iconify-json/simple-icons inlined the whole pack into the build's virtual module, measured 2,029,251 to 6,752,970 bytes of build-time source, to draw seven chips in Compat.astro. The cost is invisible in the output, which is how it would compound with each brand pack anyone adds. Collections left unnamed (tabler) still get the whole pack.
The hero became "Your machines. Your agents. One control plane." and the card kept saying "Every agent. Every machine. One screen." — so every shared link sold a headline no page carries, and the og:image:alt beside it repeated the retired line to anyone reading with a screen reader.

New filename rather than a re-shoot in place, which is the rule Seo.astro already states: scrapers cache og:image by URL, so overwriting one-screen.png would have left the superseded card in previews for as long as they hold it. The old PNG stays for the same reason — a scraper still on the old URL re-fetches it, and deleting the file turns those previews into a broken image rather than an out-of-date one. That is now written down, because an unreferenced binary is exactly what a later cleanup deletes.

2.5rem, down from 2.75: the new headline is a sentence longer, and at the old size "Your machines. Your agents." no longer cleared the 39rem column on one line. The third line that bought would have pushed the ProofCard's amber wake off the bottom edge, which is the one row the card's composition has no slack for.

And a contract test that the og:image the meta tag names is in the build at all. The filename tracks the card's claim, so every recut edits a string in Seo.astro that nothing checked; getting it wrong 404s the card on every page at once while the rest of the site stays green. It paid for itself on the first run — it caught a stale preview server serving a dist built before the recut.
The comment I added a commit ago said device_cap_dialog.dart "takes a founding-price waitlist signup". It does not: it opens antgrid.ai/pricing externally, with an open-external glyph on the button and a line above saying why the button is a waitlist and not a purchase. The app captures no address anywhere.

Which is the more useful fact for the next reader of this file, because it is the pattern this screen is missing rather than an argument for building a fourth capture: the app already has an answer to "the paid path cannot be bought yet", one screen over, and it is a link out.
@bharathm03 bharathm03 added the changelog-ignore Not user-facing: keep this PR out of the generated release notes label Sep 2, 2026
The drawer now shows multiple machines (This machine, macbook-pro,
prod-box), matching drawer_entry_row.dart's local/expanded/collapsed
band semantics. The shared agent pane renders a terminal transcript
instead of a chat transcript, since Terminal is the app's default mode
and Chat is still alpha (mode_segmented.dart).

Handler's escalation copy was updated to match what the terminal
actually shows: no test command ran, three file edits, and the
auto-answer is the write permission the transcript records rather than
a rerun that never happened. Fleet's composer chip now names a machine
(studio-workstation) instead of a generic 'Local', consistent with the
drawer's multi-machine framing.
The rail column stretches to match its taller siblings, but the
footer just followed the list instead of the stretched box, so it
sat a few rows down with a dead gap beneath it instead of flush
against the window's bottom edge like the real drawer's account row.
@bharathm03
bharathm03 merged commit bd5d810 into development Sep 2, 2026
5 checks passed
@bharathm03
bharathm03 deleted the feat/site-copy-and-agent-compat branch September 2, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog-ignore Not user-facing: keep this PR out of the generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant