/ technical docs
hack the hill · general project · technical documentation

how alten
is built.

alten is an AI copilot for older adults browsing the web. You tap a smiley buddy, say what you want to do in your own language, and alten points at the exact button to press, says the step out loud, and waits for you. It stops you before scam links, and it gives your family a calm dashboard of how the week went.

Chrome extension · MV3Next.js 16 · VercelTiger Cloud · TimescaleDBElevenLabsGemma 4 / Geminialten.selectSep 2026

01Overview

Getting stuck online is rarely about not being able to click. It's about not knowing which thing to click, being afraid of pressing the wrong one, and having nobody to ask. The usual fixes all have a catch: a grandkid on the phone isn't always free, remote-desktop tools take control away from the person, and AI agents that do the task for you teach nothing and can be steered into paying the wrong site.

alten takes the other path: the person stays in control, and alten only points. It never clicks, types, or sends anything. The one thing it does itself is open a website the person names.

The product has three surfaces:

~5,100lines of extension JS across 13 modules
14dashboard API routes, all zod-validated
4 + 3hypertables + continuous aggregates on Tiger
3ElevenLabs APIs: TTS, Scribe STT, voice library

02Prize tracks at a glance

Each card links to the section with the full technical detail and the files that prove it.

03Architecture

Everything the senior touches runs inside their own Chrome. The extension reads the page locally, asks the planner model for one next step, and talks to ElevenLabs for voice. It reports help sessions, scam warnings and heartbeats to the dashboard API, which stores them in Tiger Cloud. The family reaches everything through alten.select.

architecture.svg
senior's chrome alten extension (MV3) scanner · planner · buddy · voice · scam family's browser alten.select landing · /dashboard · /docs · /api Gemma 4 26B Google model via Logfare (OpenAI API) ElevenLabs Scribe v1 STT Multilingual v2 TTS events heartbeats settings + shortcuts sync Vercel · alten-landing Vite + React · rewrites /dashboard/*, /api/* Vercel · alten-dashboard Next.js 16 App Router · basePath /dashboard 14 API routes · zod · HMAC sessions Tiger Cloud · TimescaleDB 4 hypertables · 3 continuous aggregates Resend branded scam alert emails email to family
PieceRuns onJobOwner
ExtensionThe senior's Chrome (Manifest V3)Read the page, plan one step, point, speak, guard against scamsSamith
PlannerGemma 4 26B via Logfare; StepFlash fallbackTurn goal + page into one JSON actionSamith
VoiceElevenLabsSpeech in (Scribe), speech out (Multilingual v2), voice librarySamith + Aryan
Dashboard + APINext.js 16 on VercelPairing, event ingest, recap, alerts, shortcuts, settings, emailAryan
DatabaseTiger Cloud (TimescaleDB 2.x)Time-series storage, aggregates, retention, rate limitsAryan
Landing + docsVite + React on Vercelalten.select, routing, real demo recordingsAryan

04The Chrome extension

A Manifest V3 extension: a service worker (background.js) that owns every network call, plus content scripts injected on all http(s) pages. The heavy lifting is local JavaScript; the model only ever sees a compact text description of the page.

ModuleLinesResponsibility
panel.js1,562The guidance loop: goals, steps, confirmation, choices, done detection, navigation, session persistence
scanner.js1,338Turns the live DOM into a numbered list of real, visible, topmost controls; watches clicks and fields
buddy.js773The smiley buddy: dock, chat tray, flying pointer, captions, Yes/No, shortcut tray
voice.js335Mic capture, silence detection, ElevenLabs Scribe STT, browser fallback, TTS playback
background.js297Planner client (system prompt, retries, model fallthrough), TTS, tab routing, commands
alten-api.js199Pairing, device token, event queue, heartbeats, settings/shortcuts sync
scam.js163Lookalike / download / wording checks and reporting
languages.js, family.js, popup.js, …~450Language rules, family shortcuts, the pairing-code popup, options

4.1 One step, end to end

  1. Ask. The person taps the buddy (or presses the listen shortcut) and speaks, or types in the tray. Speech goes to ElevenLabs Scribe (§6).
  2. Shortcut the obvious. If the goal names a site ("go to cbc.ca") and we're not on it, namedSiteUrl() opens it directly. That's the only action alten takes for the person.
  3. Scan. scanner.js builds the page model: URL, title, focused field, filled fields, open popups, and a numbered PICK ONE list of controls (§4.2).
  4. Plan. The service worker sends goal + history + page model to the planner and gets back exactly one JSON action (§5). remapTarget() re-resolves the target from the words in the instruction itself (preferring controls inside an open popup, breaking ties between same-named controls, and refusing Close/X on a login gate), so a stale or off-by-one index can't point at the wrong button.
  5. Guard. Before pointing at a link or opening a URL, judgeTarget() runs the scam checks. A hit replaces the pointer with "Stop. That looks like a scam. Don't tap it." (spoken) and notifies the family (§4.3).
  6. Point and speak. The buddy flies to the control, the highlighter outlines it, a caption names it in quotes ("Tap “Compose”."), and the line is spoken with ElevenLabs.
  7. Wait for the human. watchClick() / watchField() listen for the person's own click or typing, including inside same-origin iframes. Big steps get a Yes/No first; open choices (sign-in method, size, seat) get choose, which points at the options and lets them pick.
  8. Check and repeat. Re-scan. looksDone() checks the page for real outcomes ("message sent", "added to cart", a booking confirmation) so alten stops when the job is done, not when a form merely appears.

4.2 Page scanner

Real websites are hostile to automation: overlays, cookie walls, icon-only buttons, calendars with 60 identical "12"s, shops that render menus in iframes. The scanner's job is to hand the model a list it can't misread.

// What the planner actually receives (trimmed)
GOAL: send an email to my grandson aryan saying happy birthday
ON: mail.example.com · FOCUS: none · FILLED: none
PICK ONE:
0. button "Compose"
1. link "Inbox"
2. row "Sender 1 · Newsletter 1 — weekly update"
…

4.3 Scam guard

scam.js judges any link or URL before alten will point at it or navigate to it. It's deliberately explainable: every warning carries plain-language reasons that end up in the family alert.

4.4 Sessions across pages and tabs

Most tasks cross page loads. Every step is persisted to chrome.storage (goal, history, step, pending confirmation, tab id) and resumed by the content script on the next page via continueSession(). stillOnTask() compares sites so an unrelated page doesn't resume an old goal, and gb.return brings the person back to the original tab when a flow opens a new one. A fresh browser start clears the session but keeps pairing and family settings.

05AI planner: Google Gemini → Gemma 4

The planner turns "what they want" plus "what's on screen" into exactly one next action. We built it on Google's models from the first commit.

How it evolved

Request contract

POST https://logfare.ai/v1/chat/completions
{ "model": "gemma-4-26b", "temperature": 0.1, "max_tokens": 180,
  "response_format": { "type": "json_object" },
  "messages": [ system rules, language note, …history, page model ] }

// The only shape the model may answer with
{ "action": "click|type|select|scroll|say|navigate|choose|done",
  "target_index": 0, "url": "", "text": "", "instruction": "",
  "confirm": false, "choices": [], "done": false, "message": "" }

Making a model safe to follow

06Voice: ElevenLabs

For a lot of older adults, reading small UI text is the hard part. Voice isn't a feature bolted on; it's the main channel, in both directions, in the person's own language. We use three ElevenLabs APIs.

Speech in: Scribe (scribe_v1)

Speech out: Multilingual v2 (eleven_multilingual_v2)

POST /v1/text-to-speech/{voice_id}?output_format=mp3_44100_64
{ "model_id": "eleven_multilingual_v2",
  "voice_settings": { "stability": 0.32, "similarity_boost": 0.78, "style": 0.42,
                      "use_speaker_boost": true, "speed": 0.85 } }

The voice library, in the family dashboard

One pipeline, many languages

Settings have two independent fields: speak_language (what alten says; default auto) and listen_language (what Scribe expects; defaults to the same). With auto, a question asked in Spanish is transcribed by Scribe, answered by the planner in Spanish, and spoken by Multilingual v2 in Spanish, while button names stay exactly as they appear on screen so the person can still find them. Our demo shows a coffee order asked as "pídeme un café con leche helado grande" and guided with "Haz clic en “Large”".

07Data: Tiger Data

Everything alten records is an event in time: a question, a step, a click, a scam warning, a heartbeat. So the whole backend is designed around TimescaleDB on Tiger Cloud rather than treating it as plain Postgres.

Schema

TableTypeTimescale features
help_eventshypertabledefault chunks; compressed after 7 days, segmentby = senior_id, orderby = time DESC
scam_eventshypertableseverity, reason, outcome, notified flag for email dedupe
heartbeatshypertable1-day chunks; 7-day retention policy drops whole chunks
api_attemptshypertable1-day chunks; 1-day retention; powers rate limiting
seniors, family_members, family_links, shortcuts, settingsregularrelational data, FKs with cascade

Continuous aggregates, in real time

CREATE MATERIALIZED VIEW weekly_help
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
SELECT time_bucket('1 week', time) AS week, senior_id,
       count(*) FILTER (WHERE event_type = 'question')  AS questions,
       count(*) FILTER (WHERE event_type = 'completed') AS completed,
       count(*) FILTER (WHERE event_type = 'abandoned') AS abandoned
FROM help_events GROUP BY week, senior_id WITH NO DATA;

SELECT add_continuous_aggregate_policy('weekly_help',
  start_offset => INTERVAL '8 weeks', end_offset => INTERVAL '1 minute',
  schedule_interval => INTERVAL '5 minutes');

A rate limiter on a hypertable

Pairing and linking are brute-forceable (a 6-digit code), so every attempt is one row in api_attempts with a sha256 of the IP or device, never the raw value. One statement inserts and counts:

WITH ins AS (INSERT INTO api_attempts (bucket, key) VALUES ($1, $2))
SELECT count(*)::int AS n FROM api_attempts
WHERE bucket = $1 AND key = $2
  AND time > now() - make_interval(mins => $3);

Because it lives in the database rather than memory, the limit holds across every serverless instance. An index on (bucket, key, time DESC) keeps the lookback cheap, and the 1-day retention policy means cleanup is a chunk drop instead of a DELETE.

Operations

08Family dashboard & API

A Next.js 16 App Router app served under basePath: "/dashboard". API routes return { ok, data } or { ok, error } through one wrapper that turns zod, auth and JSON errors into clean responses.

RouteCallerPurpose
POST /api/pair/create, /pair/codeextensionCreate a senior + device token; issue a fresh 6-digit code (24 h expiry)
POST /api/linkfamilyEnter the code, create/link the family member, set the session cookie (rate limited)
POST /api/eventsextensionBatched help / scam / heartbeat ingest; accepts valid events, reports rejected ones
GET /api/shortcuts, /settingsextensionSync family shortcuts and settings to the device
GET /api/seniors, /log, /alerts, /recapfamilyStatus (online, current site), session history, scam alerts, weekly recap
/api/shortcuts/manage, /settings/managefamilyCreate, edit, reorder shortcuts; voice, speed, languages, highlight size, scam sensitivity
GET /api/voicesfamilyElevenLabs voice library for the picker
POST /api/email/unsubscribemail clientsRFC 8058 one-click unsubscribe for alert emails

Scam alert emails

Medium and high scam events trigger a branded email to each linked family member through Resend's batch API, sent after the response with Next's after() so email can never slow or fail ingest. Each email is personal (own unsubscribe link), carries List-Unsubscribe + List-Unsubscribe-Post headers and an idempotency key, and a site already emailed about in the last hour is skipped so one page tripping the guard ten times sends one email.

09Domain: alten.select

.select is a top-level domain operated by GoDaddy Registry. IANA lists its registry operator as Registry Services, LLC, with GoDaddy Registry as administrative and technical contact. We registered alten.select through Porkbun, and the name is the pitch: alten helps you select the right thing on the page.

One domain carries the entire product:

URLServed byHow
alten.select/alten-landing (Vercel)Apex A record to Vercel; the landing project owns the domain
alten.select/docsalten-landingThis page, a static file in the landing build
alten.select/dashboard/*alten-dashboard (Vercel)Landing vercel.json rewrites to the dashboard, which runs with basePath: /dashboard
alten.select/api/*alten-dashboardRewritten to /dashboard/api/* so older extension builds keep working
alerts@alten.selectResendSending domain with DKIM (resend._domainkey) and SPF/MX on send.alten.select
// landing page v1/vercel.json: multi-zone routing on one domain
{ "rewrites": [
  { "source": "/dashboard/:path*", "destination": "https://<dashboard-project>/dashboard/:path*?__via=alten" },
  { "source": "/api/:path*",       "destination": "https://<dashboard-project>/dashboard/api/:path*?__via=alten" } ] }

alten.select is the only public address. The landing project redirects its own Vercel host and www to the apex, and the dashboard's proxy.ts redirects any page opened directly on its Vercel host to alten.select/dashboard. Requests forwarded by alten.select carry a __via marker so they are never bounced, and API routes are exempt so extensions installed before the switch keep working.

Because the dashboard lives under a path on the same origin rather than a subdomain, the family's session cookie, the email links, the unsubscribe page and the docs all share one trustworthy address, the one that's printed on every email and spoken by nobody but alten.

10UI/UX

We designed for a specific person: 78, comfortable with email, nervous about everything else, reading glasses somewhere nearby. Every decision below traces back to her.

The extension

The family dashboard

Accessibility across the product

11Security & privacy

12Testing & tooling

13Stack reference

LayerTechnology
ExtensionChrome Manifest V3, vanilla JS content scripts + service worker, Web Audio, MediaRecorder
AIGoogle Gemma 4 26B (live, via Logfare); Google Gemini API (v1); StepFlash fallback
VoiceElevenLabs Scribe v1, Multilingual v2, Voices API
DashboardNext.js 16.3 (App Router, Turbopack), React 19, Tailwind CSS v4, zod 4, html-to-image, lucide
DatabaseTiger Cloud (TimescaleDB): hypertables, continuous aggregates, compression, retention; postgres driver
EmailResend (batch API, one-click unsubscribe)
LandingVite 8, React 19, Tailwind CSS v4
HostingVercel (two projects, multi-zone rewrites); domain alten.select (GoDaddy Registry TLD)
ToolingTypeScript 5, ESLint 9, tsx, Puppeteer + Chrome for Testing, ffmpeg