Skip to content
Getting Started

Getting Started

Prerequisites

  • Go 1.26+ (go.mod pins go 1.26.1)
  • Node 18+, only for make test-js (the admin pages’ browser-side tests)
  • NVIDIA API key for AI generation and critique (free tier available at build.nvidia.com)
  • Optional: Unsplash and Pexels API keys for the image pipeline (Openverse and Wikimedia Commons need none)
  • Optional: an SMTP relay, for confirmation emails and the newsletter

Quick Start

# Clone the repository
git clone https://github.com/anik-apps/milesahead.git
cd milesahead

# Copy and edit the configuration
cp config.example.yaml config.yaml
# Edit config.yaml with your API key and admin password hash

# Build and run
go build -o milesahead ./cmd/milesahead
./milesahead -config config.yaml

The server listens on server.port (config.example.yaml uses 8080; production uses 9090). Visit / for the public site and /admin/login for the admin dashboard. The SQLite database is created on first run, with every table and index.

To check a config file without starting anything (the deploy runs this on the config it generates):

./milesahead -check-config -config config.yaml   # "config ok: ..." or exit 1

Configuration

All settings live in a single config.yaml file. config.example.yaml documents every key, and the reasons behind the production values. A value that is exactly ${VAR_NAME} is replaced with that environment variable after the file is parsed; ${...} inside a longer value is left alone, so secret contents are never expanded again.

SectionKeyDescription
serverportHTTP port
serverdomainPublic domain name, used for absolute links (email, feeds, og tags)
adminpassword_hashbcrypt hash of the admin password
dbpathPath to the SQLite database file
scannerdefault_intervalMinutes between scans (180 in production)
scannercluster_thresholdMinimum title similarity for two posts to be one story (0.30; 0 uses cluster.DefaultThreshold)
scanneruser_agentHTTP User-Agent for scanning
scannerstory_dedupStory-level duplicate check: shadow (default), block or off
aigenerator_modelModel for article generation (nvidia/nemotron-3-ultra-550b-a55b)
aifallback_generator_modelComma-separated chain tried when the generator fails or is retired
aicritic_modelModel, or comma-separated chain tried in order, for verification only (nvidia/nemotron-3-super-120b-a12b,moonshotai/kimi-k3)
aicritic_disable_thinkingSend enable_thinking: false on critic requests only (default false; production true)
aicritic2_modelOptional second critic; empty in production
aivision_modelImage relevance scoring (default meta/llama-3.2-11b-vision-instruct)
airequest_timeoutSeconds per generation or verification call (900 in production)
aienrichment_modelComma-separated chain of fast models for every short AI call: story dedup, enrichment, image text-scoring, newsletter pick, card-update detection (openai/gpt-oss-20b,nvidia/nemotron-3-super-120b-a12b). Unset, the generator fallbacks then the critics are used
aienrichment_intervalMinutes between enrichment passes (default 15)
aienrichment_max_attemptsPasses before an article is left alone (3)
aienrichment_max_durationMinutes one enrichment pass may run (10)
aihf_api_tokenNVIDIA API key
aihf_base_urlAPI base URL (https://integrate.api.nvidia.com/v1)
aicritic_thresholdScore at or above which an article is saved as pending rather than draft. There is no default in code, so set it (production 75)
aimax_retriesRetries per model call on a retryable error
aicache_ttlAI response cache TTL in seconds
aiunsplash_keyUnsplash API key for image search
aipexels_keyPexels API key for image search
mailsmtp.host, smtp.port, smtp.user, smtp.password, smtp.fromOutbound email; see Email
mailimap.*Mailbox for review-by-email replies. Review by email is not built, so the poller never starts; startup logs review by email is off (mail.imap not configured) with the deploy’s placeholders, and the settings page says “Review by email: off”
newslettersend_dayDay the weekly issue goes out: a weekday name or its first three letters (default saturday). The issue is built at 20:00 the day before
newslettersend_timeTime it goes out, 24-hour HH:MM (default 08:00)
newslettertimezoneIANA zone for send_day and send_time, and for which day counts as “today” for issue dates (default America/New_York). The sign-up page, confirmation email and admin pages say the schedule in words (“every Saturday at 8:00 AM ET”). A value that cannot be read fails the config check
newslettersignup_enabledOpens newsletter sign-up (default false; on in production). See Newsletter
newsletterunsubscribe_mailboxAdds a mailto: to List-Unsubscribe; leave empty unless someone reads that mailbox
votesalert_thresholdMinimum downvotes in the window before alerting
votesalert_ratioShare of the window’s votes that must be downvotes
votesalert_windowHours of votes the alert check counts (production 24; 0 counts all votes)
votesalert_emailWhere vote alerts are also emailed (the ALERT_EMAIL deploy secret). Empty by default: alerts are shown on the admin Published page only
sitepersona_nameAI persona name (default Scout)

Admin Password Hash

Generate a bcrypt hash for your admin password:

htpasswd -nbBC 12 "" "your-password" | tr -d ':\n' | sed 's/$2y/$2a/'

Or use any bcrypt generator and paste the hash into config.yaml under admin.password_hash.

Seeding Reference Pages

The project ships with 53 credit card reference pages in cmd/seed-cards/cards.json, and their structured comparison data in cmd/seed-structured/structured-data.json. Seed them into the database:

go run ./cmd/seed-cards -db ./data/milesahead.db
go run ./cmd/seed-structured -db ./data/milesahead.db

Both skip cards that already exist. Use -force to overwrite, and -slug to limit that to named cards:

# Overwrite only named cards -- -force alone rewrites every card
go run ./cmd/seed-cards -db ./data/milesahead.db -force -slug amex-platinum,amex-green

Tests and local previews load the same card data, plus 22 made-up articles, through internal/seeddata, never a copy of the production database.

Adding Sources

Sources are managed on the admin Sources page. Each source needs:

  • Name – display name, used internally (articles never name it)
  • URL – base URL of the blog
  • RSS URL (optional) – RSS feed URL for scanning
  • Scrape Selector (optional) – CSS selector for HTML scraping fallback
  • Scan Interval (optional) – override the default interval in minutes

The page shows each source’s last article, articles this week and total, and tints sources with nothing new for days. Every source, active or not, is on the list of outlets articles must not name; see AI Pipeline.