Can you guess a symbol before it's generated? Precoggers runs a daily experiment: pick one of 20 icons for two independent targets (market and random), each with two guess windows (primary before midnight, secondary before 9:30 AM). After 4 PM the results are revealed and your hit rate is tracked against the 5% baseline you'd expect from pure chance.
"I built an app where I guess tomorrow's symbol before it's generated, and the actual symbol gets determined later either by the stock market or a random generator. Over time it tracks whether I'm beating the odds or just guessing like a normal human with an overconfident brain."
circle · triangle · square · star · wave · spiral · heart · skull · tree · lightning · eye · key · sun · moon · mountain · feather · arrow · hourglass · crown · flame
| Mode | How the target is picked |
|---|---|
| Market | SHA-256 hash of {date}_{SPY closing price} → deterministic, verifiable |
| Random | secrets.token_hex(16) seed → SHA-256 → icon index, seed published for verification |
| Guess type | Deadline | What it tests |
|---|---|---|
| Primary | Midnight | Pure precognition — result not yet determined |
| Secondary | 9:30 AM | Last chance before market opens |
Four slots per day total: market-primary, market-secondary, random-primary, random-secondary.
| Time | Event |
|---|---|
| 00:00 | Primary guesses lock |
| 09:00 | Reminder logged |
| 09:30 | Secondary guesses lock |
| 16:05 | SPY price fetched via yfinance; both results revealed |
# 1. Install dependencies
cd backend
pip install -r requirements.txt
# 2. Initialize the database (safe to re-run — migrates existing data)
python init_db.py
# 3. Start the server
uvicorn main:app --host 0.0.0.0 --port 8000
# 4. Open the frontend
# Open frontend/index.html in a browser, or:
cd ../frontend && python -m http.server 8080The backend runs on http://localhost:8000. The frontend expects it there and calls it directly.
Precoggers/
├── backend/
│ ├── main.py # FastAPI app factory — mounts routers, starts scheduler
│ ├── db.py # Database class + BinomialStats
│ ├── scheduler.py # APScheduler — 4 daily jobs
│ ├── init_db.py # DB init + migration (adds guess_type/locked to old DBs)
│ ├── requirements.txt
│ └── routes/
│ ├── guesses.py # POST /guess/{mode}/{guess_type}, GET /today/guesses
│ ├── results.py # POST /result/{market|random}, GET /today/results, GET /yesterday
│ ├── stats.py # GET /stats/*, GET /stats/by-guess-type
│ └── history.py # GET /history
│ └── tests/
│ ├── conftest.py # Fixtures: test_db, client
│ ├── test_db.py
│ ├── test_guesses.py
│ ├── test_results.py
│ └── test_scheduler.py
├── frontend/
│ ├── index.html # Structure only — no inline CSS or JS
│ ├── styles.css
│ ├── app.js # SPA — dashboard, guess grids, results, stats, history
│ └── icons/ # 20 SVG files (circle, triangle, square, ...)
└── precoggers.db # SQLite — created on first run
POST /guess/{mode}/{guess_type} mode: market|random guess_type: primary|secondary
Body: { "image_id": 7 }
Response:
{
"already_locked": false,
"guess": { "id": 1, "target_date": "2026-06-13", "mode": "market",
"guess_type": "primary", "image_id": 7, "image_name": "heart", "locked": 0 },
"target_date": "2026-06-13"
}
GET /today/guesses
Response: { "target_date": "2026-06-13", "slots": {
"market_primary": {...}, "market_secondary": null,
"random_primary": {...}, "random_secondary": null } }
POST /result/random Reveals today's random result (idempotent)
POST /result/market
Body: { "spy_close": 527.43 } Manual override — scheduler handles this automatically
GET /today/results Results annotated with primary_guess + secondary_guess per result
GET /yesterday
GET /stats/binomial Overall z-score, p-value, confidence interval
GET /stats/by-guess-type Accuracy breakdown: market_primary, market_secondary, ...
GET /stats/rolling?window_days=14 Per-day accuracy over recent window
GET /stats/expected-vs-actual?days=30
GET /history?days=60
CREATE TABLE guesses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
target_date DATE NOT NULL,
mode TEXT NOT NULL CHECK(mode IN ('market', 'random')),
guess_type TEXT NOT NULL CHECK(guess_type IN ('primary', 'secondary')),
image_id INTEGER NOT NULL REFERENCES images(id),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
locked BOOLEAN NOT NULL DEFAULT 0,
UNIQUE(target_date, mode, guess_type)
);
CREATE TABLE results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date DATE NOT NULL,
mode TEXT NOT NULL CHECK(mode IN ('market', 'random')),
seed_source TEXT NOT NULL, -- "SPY=527.43" or "secure_rng"
hash TEXT NOT NULL UNIQUE,
image_id INTEGER NOT NULL REFERENCES images(id),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date, mode)
);Existing databases with the old single-guess schema are automatically migrated by init_db.py (existing rows get guess_type = 'primary', locked = 0).
The null hypothesis is 5% accuracy (1 in 20 icons). The app tracks:
- Z-score — standard deviations above/below chance
- P-value — probability of results this good occurring by luck
- Wilson 95% CI — confidence interval on true hit rate
- By-guess-type breakdown — primary vs secondary, market vs random independently
Rough significance thresholds:
| Z-score | Interpretation |
|---|---|
| < 1.645 | Within random variation |
| ~2σ | Marginal (p ≈ 0.05) |
| ~2.5σ | Significant (p ≈ 0.01) |
| 3σ+ | Strong evidence |
You need roughly 100–300 days of data for meaningful conclusions at 5% base rate.
Both result types publish a seed and hash so the outcome can be independently verified:
Market: seed = "{date}_{spy_close:.2f}" → SHA-256 → byte[0] % 20 → icon index
Random: seed = secrets.token_hex(16) (published in seed_source) → SHA-256 → byte[0] % 20 → icon index. The hash column stores sha256(seed).hexdigest()[:16] so you can confirm the stored hash matches the published seed.
cd backend
pytest tests/ -v35 tests covering DB methods, all four routes, scheduler jobs, and SPY fetch behavior (closed market detection).