Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture

Request flow

User query
    │
    ▼
┌─────────────────────────────────────────────────────────┐
│ handleSearch()                                          │
│                                                         │
│  1. Parse bangs ──── !g, !w, etc. ──→ 302 redirect     │
│     │ (no match)                                        │
│     ▼                                                   │
│  2. Auth check                                          │
│     │                                                   │
│     ├── No session, first visit ──→ /captcha (IQ test)  │
│     ├── No session, returning ───→ PoW challenge page   │
│     │                              (client-side mining)  │
│     ▼                                                   │
│  3. Parse search params (query, category, cookies)      │
│     │                                                   │
│     ▼                                                   │
│  4. Local cache lookup ── hit ──→ skip to step 8        │
│     │ (miss)                                            │
│     ▼                                                   │
│  5. Friend cache lookup ── hit ──→ store locally,       │
│     │ (miss)                        skip to step 8      │
│     ▼                                                   │
│  6. Fanout (singleflight-coalesced)                     │
│     ├── Engine 1 ──→ goroutine ──→ HTTP scrape/API      │
│     ├── Engine 2 ──→ goroutine ──→ HTTP scrape/API      │
│     ├── Engine N ──→ goroutine ──→ HTTP scrape/API      │
│     └── Friends  ──→ goroutine ──→ peer cache query     │
│     │                                                   │
│     │  hard window (1000ms default) or                  │
│     │  idle timeout (500ms) with must-include gate       │
│     ▼                                                   │
│  7. Merge + deduplicate + rank                          │
│     ├── URL normalization + dedup                       │
│     ├── Cross-engine consensus scoring                  │
│     ├── Per-engine weight × position decay              │
│     ├── Intent-based boost (code, academic, images...)  │
│     ├── Tracking param stripping                        │
│     └── Store merged results in cache                   │
│     │                                                   │
│     ▼                                                   │
│  8. Render                                              │
│     ├── Check oracles (calculator, unit, timezone, etc.)│
│     ├── URL rewriting (YouTube→Invidious, etc.)         │
│     ├── Auto-translate if foreign query detected        │
│     ├── Log search (if history enabled)                 │
│     └── Render HTML template with results + timeline    │
│                                                         │
│  9. Push to peer caches (background goroutine)          │
└─────────────────────────────────────────────────────────┘

Auth flow

New visitor                 Returning visitor
    │                            │
    ▼                            ▼
┌────────────┐           ┌─────────────┐
│ IQ Test    │           │ PoW WASM    │
│ (captcha)  │           │ (SHA-256    │
│ Image grid │           │  mining)    │
└────┬───────┘           └──────┬──────┘
     │ correct                  │ valid nonce
     ▼                          ▼
┌──────────────────────────────────────┐
│ Ed25519-signed session cookie        │
│ (AuthCaptcha or AuthPow)             │
│ TTL: captcha=24h, pow=5min           │
└──────────────────────────────────────┘

Owner: POST /admin/login with owner_secret
       → AuthOwner session cookie

Groups: cookie hash → group lookup (groups.kdl)
        → permissions: sidecar, server_api_keys,
          friend_proxy, priority_fanout

Federation protocol

Instance A                         Instance B
    │                                   │
    │  GET /api/v1/cache               │
    │  ?q=QUERY&cat=web                │
    │  Authorization: Bearer PEER_SEC  │
    │ ─────────────────────────────→   │
    │                                   │
    │  200 OK (cached results)          │
    │  or 204 No Content               │
    │ ←─────────────────────────────   │
    │                                   │
    │  POST /api/v1/cache              │
    │  (push fresh results to friend)  │
    │ ─────────────────────────────→   │
    │                                   │
    │  POST /api/v1/proxy              │
    │  {"service":"translation",       │
    │   "params":{...}}                │
    │ ─────────────────────────────→   │
    │                                   │
    │  200 OK (translation result)     │
    │ ←─────────────────────────────   │
    │                                   │
    │  GET /ami4got                    │
    │ ─────────────────────────────→   │
    │  {"version":"2.0",               │
    │   "engine_count":15, ...}        │
    │ ←─────────────────────────────   │

Proxyable services: translation, wolfram, autocomplete
Auth: shared peer_secret as Bearer token
Wait policy: configurable (all friends, N friends, miss-ok)
Failure tracking: auto-mark down at threshold (e.g. 1% over 1d)

AI research agent loop

User searches same query twice
         │
         ▼
GET /api/v1/research?q=QUERY  (SSE stream)
         │
         ▼
┌─────────────────────────────────────────┐
│ RunAIResearch()                         │
│                                         │
│  System prompt (data/metaprompt.md)     │
│  + top search result URLs               │
│         │                               │
│         ▼                               │
│  LLM generates tool calls:             │
│  ┌──────────────────────────────────┐   │
│  │ fetch(url)  → HTTP GET + strip   │   │
│  │ lua(code)   → sandboxed Lua VM   │   │
│  │ send(text)  → SSE "message"      │   │
│  │ eta(s,job)  → SSE "eta"          │   │
│  │ pin(text)   → permanent context  │   │
│  │ forget(id)  → evict old result   │   │
│  │ friction(s) → operator feedback  │   │
│  └──────────────────────────────────┘   │
│         │                               │
│         │ (loop until done or timeout)  │
│         ▼                               │
│  SSE "done" event with final answer     │
│                                         │
│  Cache result for replay               │
└─────────────────────────────────────────┘

Provider: any OpenAI-compatible API
          (Cerebras, OpenRouter, Ollama, custom)
Sandbox:  Lua (recommended) or bash (containers only)
Timeout:  max-runtime-seconds (default 60)
MCP:      optional external tool servers via stdio/HTTP

File organization

Go server (go-server/)

FileLinesPurpose
main.go503Entry point, config loading, startup sequence
routes.go364HTTP route registration table
handlers_search.go455Search and landing page handlers
handlers_settings.go281Settings page GET/POST
handlers_auth.go280Captcha and PoW handlers
handlers_admin.go~200Admin log and login
handlers_api.go~150JSON API endpoint
handlers_misc.go~100About, health, robots
admin_users.go210User group management
engine.go483Engine interface, types, registry, HTTP client
generic_engine.go453KDL-driven engine factory
engine_brave.go377Brave scraper
engine_google.go301Google scraper
engine_bing.go405Bing scraper
engine_yahoo.go351Yahoo scraper
engine_baidu.go307Baidu scraper
engine_duckduckgo.go230DDG scraper
engine_startpage.go~170Startpage scraper
engine_yandex.go~170Yandex scraper
engine_qwant.go~150Qwant API client
engine_wikipedia.go~120MediaWiki API
engine_youtube.go~130YouTube scraper
engine_hackernews.go~100HN Algolia API
engine_marginalia.go~100Marginalia API
engine_mojeek.go~100Mojeek scraper
engine_wiby.go~80Wiby API
engine_flickr.go~80Flickr API
engine_pexels.go~80Pexels API
engine_soundcloud.go~100SoundCloud API
fanout.go988Concurrent engine dispatch, scoring, dedup, merge
cache.go346In-memory LRU + TTL cache
peercache.go249Friend cache query/push
peers.go268Peer config loading, failure tracking
friendproxy.go327Service proxy between friends
auth.go316Ed25519 sessions, PoW verification
captcha_prerender.go688Pre-rendered captcha pool (IQ test grid)
captcha_image.go307Captcha image compositing
oracles.go893Calculator, unit, timezone, base, translate, DDG IA, Wolfram
oracles_extra.go852Currency, IP geo, DNS, color, packages, radio
airesearch.go1917AI agent: LLM loop, tool dispatch, Lua sandbox
airesearch_sse.go187SSE event streaming
airesearch_cache.go~100Research result caching
mcp_client.go195MCP tool server integration
translation.go614Multi-backend translation with routing
langdetect.go~100Language detection for auto-translate
autocomplete.go302Multi-backend autocomplete merger
classifier.go~100Query intent classification
imageproxy.go433Image proxy with SSRF protection
templates.go975HTML template rendering
db.go304SQLite schema, logging, queries
groups.go263User tier/group loading from KDL
tiers.go222Permission checking
rate_limit.go202Per-IP rate limiting
rewrite.go~100URL hostname rewriting
i18n.go~100Internationalization
instances.go288Instance browser + ami4got
plugins.go214Lua plugin system
selfheal.go~100Autonomous engine repair agents
sidecar.go~100Python sidecar integration
rss.go~100Atom feed generation
openapi.go530OpenAPI 3.0 spec generation
api_compat.go~1004get-compatible API
api_doc.go~100Plain-text API docs
metrics.go~100Prometheus counters
counters.go~50Atomic hit/miss counters
config_validate.go186Config validation at startup
kdl_utils.go~80KDL parsing helpers
kdlresolver.go~80KDL section resolution
helpers.go~80Cookie reading, IP extraction
stubs.go199Compile-time stubs for optional features
types.go~50Shared type definitions

Python sidecar (python-sidecar/)

FileLinesPurpose
server.py96Flask HTTP server
google.py245Google Playwright scraper
yandex.py219Yandex Playwright scraper
qwant.py259Qwant Playwright scraper

KDL configs (data/)

FileControls
config.kdlMain server configuration (port, auth, cache, API keys)
config.default.kdlDefault config template
engines.kdlAll 274 generic engine definitions
peers.kdl / peers.default.kdlFriend network topology and wait policy
groups.kdlUser tiers and permissions
captcha.kdlCaptcha pool sizes and generation cadence
autocomplete.kdlAutocomplete backend weights
translation.kdlTranslation backend priority and rate limits
instances.kdlInstance browser entries
ai-research.kdl / ai-research.default.kdlAI agent provider, model, access control
metaprompt.mdAI agent system prompt

Static assets (static/)

CSS, JavaScript (PoW WASM worker, autocomplete, SSE client), and fonts.

Templates

HTML templates rendered server-side by templates.go. No client-side framework.

Key design decisions

Single binary. The Go server compiles to one static binary (plus CGO for SQLite). No runtime dependencies except the data/ and static/ directories.

KDL over JSON for subsystems. Human-readable, comment-friendly config. The resolver supports both per-file (peers.kdl) and unified (conf.kdl with sections) layouts.

Singleflight coalescing. Concurrent identical queries share one fanout, preventing thundering herd on popular searches.

Ed25519 session cookies. Session tokens are signed, not stored server-side. No session table, no Redis.

Dual-layer anti-abuse. First-time visitors solve a visual captcha (pre-rendered, no JS required for the test itself). Returning visitors solve a WASM proof-of-work (SHA-256 mining). Both issue signed session cookies.

Oracle-first. Queries that match oracle patterns (math, units, DNS, etc.) return instant answers without waiting for engine fanout.

Friend-first cache. Before fanning out to upstream engines, 4got checks local cache, then friend caches. This minimizes upstream load across the friend network.

Design Decisions

Why federation over Tor outbound for IP-ban avoidance. Tor exit nodes are heavily flagged by upstream engines, leading to high captcha rates and slow responses. Federation distributes queries across multiple real IPs (each friend instance has its own), so no single IP gets rate-limited. Tor outbound is still available per-engine (tor-outbound true) for engines that block datacenter IPs but not Tor, but the primary strategy is the friend network.

Why low comment density is intentional. The codebase prefers well-named functions, types, and variables over inline commentary. Comments are reserved for non-obvious architectural decisions, protocol boundaries, and “why” explanations – not restating what the code already says. This keeps the signal-to-noise ratio high during code review and reduces stale-comment drift.

Why KDL over YAML/TOML/JSON for config. KDL supports inline comments, nested blocks without deep indentation, and space-separated multi-value attributes (ideal for categories "web" "news"). YAML is whitespace-sensitive and error-prone; TOML doesn’t handle nested structures cleanly; JSON forbids comments. KDL’s document-oriented model maps naturally to engine definitions and peer configs.

Why SQLite over Redis/Postgres. SQLite is embedded (no external daemon), survives restarts without a separate persistence layer, and handles 4got’s write volume (search logs, cache, stats) comfortably on a single core. The entire database is one file, trivially backed up. Redis would add an operational dependency for the cache layer alone; Postgres is overkill for a single-instance search engine.

Why single-binary Go over microservices. A metasearch engine’s subsystems (fanout, caching, auth, federation) share in-process state (the engine registry, in-memory cache, rate limiter). Splitting them into services would add network hops on the hot path and complicate deployment for self-hosters. The Python sidecar exists only for Playwright-dependent engines that need a real browser, and communicates over localhost HTTP.

Why no BM25. BM25 was implemented and tested but removed. The marginal ranking improvement over the existing position-weighted cross-engine consensus scoring was negligible, and BM25 is vulnerable to keyword stuffing in snippets – an attacker can inflate term frequency in a single engine’s results to dominate rankings. The current scoring (inverse position * engine weight * engine count * authority domain multiplier) is simpler and harder to game.

Why no click tracking. 4got is privacy-first: no result click is recorded, no outbound redirect URL wraps results, and no client-side beacon fires on click. This means ranking cannot incorporate click-through signals, which is an accepted trade-off. The honeypot link (/static/assets/beacon.js) exists solely to detect bots, not to track users.

Why engine goroutines are not cancelled after the user gets their response. When the fanout time budget expires and results are served to the user, the remaining engine goroutines continue running in the background. This is intentional: late-arriving results are merged into the cache, so the next user who searches the same query gets richer results. Cancelling goroutines would save a few connections but would mean cached results are permanently limited to whatever came back within the first time window. The cache is the product; the user’s wait time is the budget.

Why engine health monitoring is notification-based, not autonomous. Engine health snapshots (engine_snapshots.go) store the last working and last error HTML for each engine, and the notification system (notify.go) dispatches structured events to ntfy push notifications, webhooks, and a JSONL event log. Actual remediation happens outside the program — the operator hooks up whatever they want (a Claude agent, a cron job, a human reading notifications). This keeps the search engine’s scope clean: it detects problems and tells you, it doesn’t try to fix them itself. The earlier “self-healing” framing was retired in favor of this more honest and composable design.

Why adaptive PoW difficulty, not static. AdaptivePowDifficulty in auth.go scales the PoW difficulty based on current server load: +1 bit above 100 RPM, +2 above 500, +3 above 1000. This means legitimate users during low-traffic periods solve a ~1 second puzzle, while during attacks the difficulty automatically ramps up. Combined with the image captcha (first visit) and Tor-aware exemptions, this creates a graduated defense that doesn’t punish normal users for attacker behavior.