diff --git a/.gitignore b/.gitignore index b5f236da..06b8269d 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ docs/superpowers/plans/ # cli release staging — built per-release, never tracked .release-staging/ frontend/public/distrotv-cli.tar.gz + +# source bundles from scripts/make-source-zip.sh +*_Submission.zip diff --git a/CLAUDE.md b/CLAUDE.md index 2146cd50..00424b2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,14 +2,15 @@ ## Project Overview -Distro TV is an ambient **channel surface** that runs in the developer's terminal while AI coding tools work. Launch channels: **CH 01 NEWS** (HN, TechCrunch, Bloomberg, Reuters) and **CH 02 MARKETS** (stocks, crypto, FX, indices, watchlist + sparklines). Future channels (weather, build status, deploy logs, sports, calendar, crypto deep) slot into the same surface — the product is the surface, channels are the verticals. Originally launched as DevDrip (opt-in ads + USDC micropayments); pivoted to Distro TV in May 2026. +Distro is **the ad exchange for AI agent surfaces**. Agents do the work while people wait; that wait is a new attention surface with no market layer. The first surface is the developer's terminal: Distro TV shows **sponsored slots while AI coding tools work** and shares the ad revenue with the viewer (ads are the default feed). **CH 01 NEWS**, **CH 02 MARKETS** and **CH 03 UTILITIES** are opt-in channels on the same surface for people who'd rather not see ads. Long-term: an **open ad exchange for AI agent surfaces** — anyone can bid for slots the way they do for web display or a billboard; the terminal is the first inventory. History: launched as DevDrip (opt-in ads + USDC micropayments), pivoted to channels-only Distro TV in May 2026, re-centered on ads in Sep 2026 — without crypto. ## Architecture - **CLI + Daemon** — `@distrotv/cli` distributed via GitHub Releases + `curl ... | sh` install script (NOT npm). Binary: `distro`, alias `dtv`. Hooks into Claude Code via settings.json (PreToolUse, Stop, UserPromptSubmit). Daemon on Unix socket manages slot display, key capture, local ledger (SQLite). - **Backend API** — Express + Drizzle ORM + Railway Postgres + Upstash Redis. Auth, device registration, channels, watchlists, alerts, slot impression ingestion. - **Dashboard** — Next.js 14, App Router, Tailwind. Reading list, watchlist management, preferences. -- **Payments** — deferred post-M1. Base Sepolia testnet targeted for M6+. +- **Ads** — API owns supply: advertiser-written direct ads (`ad_campaigns`, portal at `/advertisers/portal`, highest bid first) → Carbon Ads (sandbox) → house-ad fallback, served as `sponsored` slots through `/me/content/next`. Each served ad is a row in `ad_impressions` (the delivery record); `/ingest` marks it seen, `GET /c/:code` (short) and `GET /ads/click/:deliveryId` record the click and redirect. See `gitbook-docs/architecture/ads.md`. +- **Payments** — none. Advertisers pay Distro, Distro pays the developer a share and keeps a margin. Earnings are **estimated** internally as `AD_CPM_RATE`/1000 × `REVENUE_SHARE_DEVELOPER` per viewable ad; neither number is shown to users. The dashboard shows a disabled "Payouts — coming soon" button. No crypto, wallets, or USDC. ## Tech Stack @@ -28,6 +29,7 @@ Distro TV is an ambient **channel surface** that runs in the developer's termina - M6: dashboard polish (shipped) - M7: admin dashboard (shipped) - M8: landing page + install vector (shipped — channels positioning, curl/GH Releases install) +- M9: ad exchange (branch `ad-exchange`) — sponsored slots on by default, estimated revenue dashboard, channels opt-in, ads-first landing + `/advertisers` exchange page. local only; not released. ## Hard Rules @@ -35,11 +37,20 @@ Distro TV is an ambient **channel surface** that runs in the developer's termina - hooks always exit 0 — never block Claude Code - local ledger is ground truth — backend can be down - no grace period before showing slots — surface as soon as Claude takes over (`GRACE_PERIOD_MS = 0`). Fast tool calls are gated by the slot vanish timer + frequency caps, not by a pre-show delay. -- **lead with _channels_ as the surface noun, never with "news + markets"** — Distro TV is a channel platform; NEWS and MARKETS are the two launch channels; future verticals slot into the same surface. Marketing, docs, and product copy must reflect this. The two-tangent "news AND market data" framing is what we explicitly pivoted away from in M8. +- **lead with the exchange, and tell the story — never pitch.** Headline: _"The ad exchange for AI agent surfaces."_ The arc: every new medium grew an ad market → AI agents created a new attention surface (the wait) → nothing serves it → Distro is the open exchange for it → the terminal is the first surface, live today. Read like infrastructure, not a consumer offer: no "get paid" headlines, no benefit-led selling. Never surface product defaults (e.g. "ads on by default") in marketing. Channels (news, markets, utilities) appear as "the slot isn't only for ads", never as the headline. Status tags must be honest: _live_ / _next_ / _planned_ — never imply bidding or self-serve exists. +- **money is always labeled _estimated_** until real payouts exist. No crypto, wallet, or USDC language anywhere. +- **never publish the split or exact rates.** The revenue structure in public copy is: _advertisers pay Distro TV, Distro TV pays the developer a share, and keeps a margin_. No "70%", no CPM figures, on the site, dashboard or CLI. The developer angle: idle agent time earns money and helps pay for the AI tools that cause it. (`REVENUE_SHARE_DEVELOPER` and `AD_CPM_RATE` stay internal.) +- **ad-platform marks (Google Ads, Meta, Amazon) appear only as monochrome icons beside their names**, in the theme's ink colour, on "import — coming soon" tiles (`frontend/components/advertisers/brand-mark.tsx`). They name a planned import, never a partnership. No full-colour logos, no marks anywhere else. Check each brand's guidelines before a public launch — Amazon in particular restricts use (its icon was pulled from simple-icons). +- **the product name is _Distro TV_, always in full** in user-facing copy (site, dashboard, CLI, docs) — never bare "Distro". Don't write "Distro is building it" style filler lines. +- **copy is short, plain and precise.** Short sentences. One idea each. Say a thing once per page: if the demo shows it, the text doesn't repeat it. The landing page is four sections — thesis, lineage, how the money moves, two doors (developers / advertisers). Don't add sections to it without removing one. +- **ad clicks go through our redirect** (`/c/:code` or `/ads/click/:deliveryId`). The redirect target comes only from the server-side delivery row, never from the request. +- **an ad pays only if it was on screen ≥ 1s and not skipped.** The same rule lives in the API (`computeEarned`) and the CLI ledger (`sumTodayOptimistic`) — change both together. +- **the sponsored panel prints the full click URL.** Terminals make a visible `http(s)://` URL cmd/ctrl-clickable; that is the only same-terminal click that needs no key capture. Never truncate it. Every panel row starts with the `▍` bar because Claude Code strips leading whitespace from status lines. - **CLI distribution = `curl -fsSL https://get.distrotv.xyz/install.sh | sh` + GitHub Releases, never npm publish.** install.sh is served from **GitHub Pages at `get.distrotv.xyz`**, NOT Vercel — Vercel's edge firewall JS-challenges `curl` (`x-vercel-mitigated: challenge`), which `curl | sh` can't solve. Source file is `frontend/public/install.sh` (single source of truth), deployed by `.github/workflows/deploy-install.yml`; it pulls the latest tarball from `releases/latest/download/distrotv-cli.tar.gz`. Releases are triggered by pushing a `cli-v*` git tag. install.sh lays out a **versioned install** — each release lands in `~/.distrotv/versions//`; `~/.distrotv/current` symlink points at the active version; shims and Claude hook entries resolve through `current/dist/index.js`. the CLI **auto-updates** on the daemon's tick: the **version signal comes from our API** (`GET /cli/version-check`, gated by `LATEST_CLI_VERSION` env var on Railway — unset = no update advertised); tarballs still live on GitHub Releases. set `LATEST_CLI_VERSION=` after each release to roll out; unset to halt. opt-out: `DISTRO_NO_AUTOUPDATE=1` or `cli.autoUpdate: false`. See `gitbook-docs/cli/releases.md`. ## Dev Rules +- local dev: `scripts/dev-link-cli.sh` points `~/.distrotv/current` + the `distro`/`dtv` shims at this checkout's CLI build (hooks, status line and daemon then run the working tree). API on `:3011`, web on `:3010`, Postgres via docker compose. - load frontend-design skill for anything frontend - minimal comments, crisp pointers, lowercase start - never mention claude or ai in commits, keep messages crisp diff --git a/README.md b/README.md index cbc53dfa..c9cada25 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,95 @@ # Distro TV -Your terminal's news + market feed, while the agent works. An ambient slot anchored at the bottom of your terminal during AI-coding-tool idle time. Tech + finance news from HN / TechCrunch / Bloomberg / Reuters. A watchlist of stocks + crypto with sparklines and key stats. +**The ad exchange for AI agent surfaces.** -## Modes +AI agents do the work. People wait and watch. That attention has no market yet. Distro TV is +that market, starting in the terminal. -- **news** — every slot is news (HN, TechCrunch, Bloomberg, Reuters, etc.) -- **markets** — every slot is a ticker from your watchlist -- **mix** — alternates news + markets; alert-driven priority bumps a ticker when it moves >5% +Every medium grew an ad market: print, broadcast, the web, mobile. Agent tools are the first +major surface with none. While a coding agent runs, a developer watches a terminal for thirty +seconds, two minutes, ten. Distro TV puts one text ad in that wait, pays the developer a share, +and clears the instant they type. -Pick one during `distro init`; flip any time with `distro preferences`. +## How it works -## Structure +``` +advertiser ──writes an ad, sets a bid──▶ Distro TV ──serves by bid──▶ developer's terminal + ▲ │ │ + └────── views · clicks · spend ─────────┴──── pays the viewer a share ◀┘ +``` + +1. **Advertisers pay Distro TV.** They write a one-line text ad in the portal and set a CPM bid. + Only views of a second or more count. +2. **Distro TV runs the exchange.** Highest bid first, then network fill (Carbon Ads sandbox), + then house ads. It keeps a margin. +3. **Developers get paid.** Idle agent time earns money. It helps pay for the AI tools that + cause it. + +Opt-in only. The slot clears when you type. Prefer no ads? Tune the slot to news or markets. + +## What is in the box + +| Part | What it does | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `packages/cli` | `distro` / `dtv`. Hooks into Claude Code, runs a small daemon, renders the ad in the status line, records views in a local SQLite ledger, syncs them within seconds. | +| `packages/api` | Express + Postgres. Ad supply, delivery records, the pay rule, click redirects, earnings, the advertiser API. | +| `frontend` | Next.js. Landing page, developer dashboard with a live revenue page, advertiser page and ad portal. | +| `packages/shared` | Types and constants shared by all three. | + +The ad, as a developer sees it: ``` -frontend/ # Next.js landing + dashboard (Vercel) -packages/ - cli/ # @distrotv/cli — Claude Code hooks + daemon + slot renderer - api/ # Express backend — auth, channels, watchlists, alerts - shared/ # @distrotv/shared — slot payload types, DTOs -docs/ # specs, design docs -gitbook-docs/ # engineering reference; mirrors github.io +▍ AD Lumen Deploy +$0.0175 +▍ Deploys that finish before your coffee does. +▍ ↗ https://api.distrotv.xyz/c/2472fa528d42 ⌘ click est. today $3.42 ``` -## Local dev quickstart (post-M1) +The link is a real URL on purpose: terminals make any visible `https://` link ⌘-clickable, so +the click works inside the same terminal with no key capture. + +## What the exchange does today + +- ad supply: advertiser-written ads served by bid, then Carbon Ads (sandbox), then house ads +- delivery records, a viewability pay rule (on screen ≥ 1s, not skipped), replay-safe ingest +- tracked click redirects with short links, and a same-terminal ⌘-click +- ads as the default feed; news and markets as opt-in channels on the same slot +- a live revenue dashboard: counting balance, per-agent-hour rate, 1h / 24h / 30d chart +- an advertiser portal: write an ad, set a bid, watch views and clicks, pause + +How it relates to AI: Distro TV is infrastructure for AI agent tools. It integrates with Claude +Code through its hooks and status line, and turns the time an agent spends working into an ad +surface. + +## Run it locally + +Needs Node 20+, pnpm, Docker. ```bash -pnpm clean && pnpm install && pnpm build -docker compose up -d postgres +pnpm install +docker compose up -d postgres # local Postgres +cp packages/api/.env.example packages/api/.env # fill in GitHub OAuth + secrets pnpm --filter @distrotv/api db:migrate -pnpm --filter @distrotv/api dev # one shell -node packages/cli/dist/index.js init # another shell -node packages/cli/dist/index.js daemon start +pnpm --filter @distrotv/api dev # api on :3011 +cd frontend && pnpm exec next dev -p 3010 # web on :3010 + +pnpm --filter @distrotv/shared build && pnpm --filter @distrotv/cli build +scripts/dev-link-cli.sh # point ~/.distrotv at this checkout +DISTRO_ENV=local dtv init # pair with GitHub, install the Claude Code hooks ``` -After M1, `distro init` writes hooks but slot rendering arrives in M3 (news) and M4 (tickers). +Open a Claude Code session and give the agent a task. The ad appears in the status line. -## Deploy +- Revenue: +- Ad portal: -- API: Railway autodeploy from `main` -- Frontend: GitHub Actions → Vercel after CI succeeds -- CLI: published to npm as `@distrotv/cli` via `pnpm release` +## Status, plainly -## Specs +- Live: terminal surface, ad serving, views and clicks measured end to end, revenue dashboard, + advertiser portal. +- Sandbox: network ads come from Carbon's demo zone. All money is an **estimate**. Nothing is + charged and nothing is paid out yet. +- Planned: importing ads from Google Ads, Meta Ads and Amazon Ads; budgets and targeting; an ad + review queue; more surfaces (IDE agent panels, agent web apps). -- Active pivot spec: `docs/superpowers/specs/2026-05-05-distro-tv-pivot-design.md` -- Plan overview: `docs/superpowers/plans/2026-05-05-distro-tv-overview.md` +More detail: [`gitbook-docs/architecture/ads.md`](gitbook-docs/architecture/ads.md) · +demo script: [`docs/demo-script.md`](docs/demo-script.md). diff --git a/docs/demo-script.md b/docs/demo-script.md new file mode 100644 index 00000000..7f330171 --- /dev/null +++ b/docs/demo-script.md @@ -0,0 +1,54 @@ +# Demo script — 3 minutes + +Pitch in one line: **developers watch a terminal for minutes at a time while their agent +works. We turn that attention into ad inventory, pay the developer for it, and open it up as +an exchange anyone can bid on.** + +## Before you present + +- [ ] `docker ps` shows `devdrip-postgres` healthy (port 5434) +- [ ] API up: `curl -s localhost:3011/health` → `"status":"ok"` (`pnpm --filter @distrotv/api dev`) +- [ ] Web up on 3010 (`cd frontend && pnpm exec next dev -p 3010`), signed in at `/dashboard` +- [ ] `dtv status` → `ads: on`, daemon running. If the CLI was rebuilt: `dtv daemon stop` +- [ ] Revenue page already has history so the chart is not empty (let an agent run for a few + minutes beforehand) +- [ ] `dtv preferences` → feeds = ads only, so the opt-in step has something to show +- [ ] Terminal font size up; window at least 100 columns wide so the ad link fits on one row +- [ ] Browser tabs ready: landing `/`, `/dashboard/revenue`, `/advertisers/portal` (signed in) +- [ ] Have one ad already running in the portal so "Your ads" shows live numbers; write the second one live + +## Script + +1. **The thesis (25s)** — landing page. Read the headline, then scroll to the lineage table: print, broadcast, web, mobile all grew an ad market; agents have "nothing yet". Type into the hero terminal so the slot clears — "this is the whole product in one gesture." +2. **The product (60s)** — in Claude Code, give the agent a real multi-step task. + - A sponsored slot appears in the status line the moment the agent takes over. + - Point at `+$0.0070` and `est. today`. It rotates every 12 seconds. + - **⌘-click the link in the ad.** The advertiser opens in the browser. No second terminal, + no key capture. + - Start typing: the ad is gone. It never competes with you for the keyboard. +3. **The money (40s)** — `/dashboard/revenue`. Estimated balance moved; ads seen, clicks, CTR, + served vs seen; **Payouts — coming soon**. Say plainly: sandbox ads, estimated CPM. +4. **The choice (30s)** — `dtv preferences` → feeds → add News. The terminal now alternates + ad / headline. "Don't want ads? Tune to a channel. Same surface." +5. **The other side (40s)** — `/advertisers/portal`. Google Ads and Meta Ads import come + first (coming soon: search ads are text, so they map one to one). Write an ad by hand: + brand, one line, link, bid. Watch the terminal-slot preview update as you type. Hit + **Run this ad** — it shows up in the terminal within a minute or so, and its views and + clicks tick up in "Your ads". A higher bid runs first and pays the developer more. + +## If something breaks + +| Symptom | Fix | +| ----------------------------- | ----------------------------------------------------------------------------------- | +| No ad in the status line | open a **new** Claude Code session; check `dtv status`; `tail ~/.distro/daemon.log` | +| Ad shows, numbers do not move | `dtv sync --force`, reload the revenue page | +| Carbon has no fill | nothing to do: house ads fill every slot | +| ⌘-click does nothing | `dtv open` from any terminal opens the ad on screen | +| API restarted mid-demo | nothing to do: deliveries live in Postgres | + +## Questions you will get + +- The ads are Carbon's sandbox zone plus demo creatives. No advertiser is paying yet. +- Advertisers pay Distro; Distro pays the developer a share and keeps a margin. The numbers on screen are estimates from a sandbox rate. The split and rates are not final, so don't quote them. Payouts are not built. +- The exchange (bidding, self-serve, account import) is the roadmap. What is built is the + supply side end to end: serving, viewability, clicks, attribution, reporting. diff --git a/frontend/app/advertisers/page.tsx b/frontend/app/advertisers/page.tsx new file mode 100644 index 00000000..478102bf --- /dev/null +++ b/frontend/app/advertisers/page.tsx @@ -0,0 +1,146 @@ +import type { Metadata } from "next" +import type { ReactNode } from "react" +import Link from "next/link" +import { Nav } from "@/components/landing/nav" +import { Footer } from "@/components/landing/footer" +import { AdComposer } from "@/components/advertisers/ad-composer" +import { BrandMark, type BrandMarkName } from "@/components/advertisers/brand-mark" + +export const metadata: Metadata = { + title: "Advertise on Distro TV — the ad exchange for AI agent surfaces", + description: + "Write an ad and run it in developers' terminals while their AI agent works. Import from Google Ads and Meta Ads soon.", + alternates: { canonical: "/advertisers" }, +} + +// monochrome marks beside the names, for familiarity — see brand-mark.tsx +const IMPORTS: { mark: BrandMarkName; name: string; body: string; when: string }[] = [ + { + mark: "googleAds", + name: "Google Ads", + body: "Search ads are text. So is the slot. They import one to one.", + when: "coming soon", + }, + { + mark: "meta", + name: "Meta Ads", + body: "Headline, text and link come across. Images stay behind.", + when: "coming soon", + }, + { mark: "amazon", name: "Amazon Ads", body: "Sponsored Brands headlines.", when: "next" }, +] + +const STEPS = [ + "You write or import an ad.", + "It shows while a developer waits on their agent.", + "You pay only for views of a second or more.", +] + +function H2({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ) +} + +export default function AdvertisersPage() { + return ( + <> +