Skip to content

feat(ads): terminal (ASCII) ad format served as plain text - #161

Merged
ralyodio merged 1 commit into
masterfrom
feat/terminal-ascii-ads
Jul 30, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/terminal-ascii-ads

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Terminals can't run /ad.js and can't render HTML, so the ad network had nothing to sell to shells, SSH banners, BBS screens, or CLIs. This adds a terminal_ascii format that is fetched, not embedded:

curl -s "https://crawlproof.com/api/ads/motd?slot=<slot_id>&cols=76"
+-- SPONSORED -------------------------------------------------------------+
|                                                                          |
| Point a Domain, Start a Pit                                              |
| Point any domain to a blacked-out, poison-green coming-soon page with an |
| email waitlist.                                                          |
|                                                                          |
| Summon it:                                                               |
| https://crawlproof.com/a/8aa098d8-e361-4fd0-b22e-a71ffe885d3c?s=motd     |
|                                                                          |
+------------------------------------------------- ads by crawlproof.com --+

What's in it

  • lib/ads/terminal.ts — fixed-width, pure-ASCII renderer. Advertiser copy is untrusted text going straight into a TTY, so escape/control chars are stripped and non-ASCII is folded (accents) or dropped (CJK, emoji) — both to keep terminals safe from cursor/screen spoofing and to keep the column maths honest.
  • GET /api/ads/motd — text/plain, ?cols=44..120, ?color=1 for ANSI truecolour, ?src=<tag> to tell surfaces apart. Impressions meter server-side through serveAd exactly like the HTML paths; an unknown or inactive slot falls back to the (unmetered) house ad so a login banner is never blank.
  • curl is not a bot, here. Terminal clients identify as curl/wget/etc., which lib/tracker/device buckets as bot — correct for a web page, wrong on this endpoint, where that is the audience. terminalDeviceType lets shell clients through and still excludes real crawlers. Without this a terminal slot could only ever serve house ads.
  • GET /a/<impression_id> — short click redirector. The long /api/ads/click?i=…&s=…&c=…&cr=… form is unusable as printed text. Resolves slot/campaign/creative from the impression row, meters via the same resolveClick, then 302s with ?ref= plus utm_source/medium/content — a shell sends no referrer, so without a tag the advertiser sees nothing. This path keeps the strict bot rule on purpose: scripted hits stay unbilled.
  • lib/ads/template.ts — {{ads}}, {{ads:64}}, {{ads:terminal:64}} token parsing so a publisher's server can place the fill inside its own template. A failed fill removes the token; a reader never sees raw {{ads}}.
  • Publisher UI — the Monetize page lists the terminal unit alongside the banner sizes with a copyable shell snippet instead of markup. It is deliberately kept out of PUBLISHER_FORMAT_IDS, so the GitHub auto-installer never injects it into HTML.
  • Migration — widens the creative format CHECK, adds terminal_ascii to slot inventory (default + backfill), and backfills a terminal creative for every existing campaign from the copy it already has: no LLM call, no advertiser action.

Already applied to prod

The migration is applied (via Supabase MCP): 25/25 campaigns now have a ready terminal_ascii creative, 23/23 slots accept the format. The code is what's still unshipped.

Testing

  • tests/contract/ads-terminal.test.ts (25) — box geometry at every width, escape/control stripping, forged-border copy, ANSI width invariance, URL never truncated, client classification.
  • tests/contract/ads-template.test.ts (14) — token grammar, aliases, clamping, dedup, failure never leaking a token.
  • Full suite: 1139 passed, tsc --noEmit clean.
  • Verified end-to-end against the real ad server + prod DB: a live paid ad filled the profullstack.com slot, and /a/<id> 302'd to https://pairux.com/?ref=crawlproof-ad-015&utm_source=crawlproof&utm_medium=terminal&utm_content=motd. The verification click was recorded unbilled (valid=false, $0) — no advertiser was charged.

🤖 Generated with Claude Code

Terminals can't run /ad.js and can't render HTML, so the ad network had
nothing to sell to shells, SSH banners, BBS screens, or CLIs. This adds a
terminal_ascii format that is fetched, not embedded:

  curl -s "https://crawlproof.com/api/ads/motd?slot=<id>"

- lib/ads/terminal.ts renders a fixed-width, pure-ASCII box. Advertiser
  copy is untrusted text going straight into a TTY, so escapes/control
  chars are stripped and non-ASCII is folded (accents) or dropped (CJK,
  emoji) — both to keep terminals safe and to keep column maths honest.
- /api/ads/motd returns text/plain, with ?cols=44..120, ?color=1 for ANSI,
  and ?src=<tag> to tell surfaces apart. Impressions meter server-side via
  serveAd exactly like the HTML paths; an unknown slot falls back to the
  house ad so a login banner is never blank.
- Terminal clients identify as curl/wget/etc., which the tracker buckets
  as "bot". On this endpoint that's the actual audience, so a dedicated
  classifier lets shell clients through while still excluding crawlers.
  The click path deliberately keeps the strict rule: scripted hits on
  /a/<id> stay unbilled.
- Clicks use a short /a/<impression_id> URL (the long query-string form is
  unusable as printed text) and carry utm_source/medium/content through to
  the advertiser, since a shell sends no referrer.
- lib/ads/template.ts parses {{ads}} / {{ads:64}} / {{ads:terminal:64}} so
  a publisher's server can place the fill in its own template.
- Migration widens the format CHECK, adds terminal_ascii to slot inventory,
  and backfills a terminal creative for every existing campaign from the
  copy it already has (no LLM call, no advertiser action).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit f148b8f into master Jul 30, 2026
7 of 8 checks passed
@ralyodio
ralyodio deleted the feat/terminal-ascii-ads branch July 30, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant