Architecture & Docs
A discovery platform for pickleball cash games. Partnership-first, scrape-last.
Ingestion pipeline
Each source is a pluggable adapter. Deterministic, source-specific parsing is preferred; unstructured discovery is a fallback. A typical run:
structured / source-specific adapters → (optional) discovery + extraction → normalize → classify → dedupe(venue/time±30m/buy-in±$5) → eligibility check → upsert → scrape_run log
The feed is amateur-first: it includes real tournaments (cash or non-cash), competitive cash games and organized ladders/KOTC — each labelled accurately (Tournament / Cash game / Competitive session, with a separate prize-status label). Clinics, lessons, camps and pure open-play/drop-in socials are excluded.
Listings we cannot verify — a stale or year-mismatched source, an untraceable price, a different sport (e.g. padel) — are deferred or rejected, never normalized speculatively into the feed.
Classification & labels
Deterministic first: structured adapter fields + regex/keyword signals (buy-in, pot, prize pool, payout, moneyball, KOTC) and an organizer allowlist. Fast, free, auditable.
Optional LLM normalization (OpenRouter): used only for unstructured discovery pages. It is opt-in and defaults OFF (OPENROUTER_NORMALIZATION_ENABLED); ordinary refresh and the whole feed run without it. It is an optional enhancement, never a required dependency.
Prize labels are evidence-based: an entry fee / buy-in is not evidence of a cash prize. "Cash prizes" is shown only with source-confirmed player-money evidence; otherwise the card reads "Prize details not published" or "Non-cash awards".
Dedupe & cross-listing
Events are blocked by normalized venue, then clustered within tolerance (start ±30 min, buy-in ±$5). Cross-listed duplicates — including the same event surfaced by several sources — collapse into one canonical event that shows all “Listed on” source badges, and a parser that confuses a purse with an entry fee is reconciled rather than double-counted.
Data source tiers
The current feed is served from a validated, source-backed inventory. Discovery adapters (Exa + Firecrawl extraction, with the optional OpenRouter normalizer) and structured-API adapters (allpickleballtournaments.com, pickleball.com) activate when their keys + flags are set. Toggle sources in Admin → Sources.
How to add a metro
1. Add the metro to ingestion/seed_data.py (name, slug, center lat/lng, timezone). 2. Add venues + organizers. 3. Add event templates or enable adapters scoped to that metro. 4. The UI, /m/[slug] route, feed, and SEO page are templated — no code changes needed.
How to add a source
1. Subclass BaseAdapter in ingestion/adapters.py. 2. Implement fetch() returning the base event shape. 3. Set source_tier. 4. Complete the robots.txt/ToS audit, then enable from Admin → Sources.
Legal & compliance
No scraping of authenticated/private pages, Meta surfaces, or Discord selfbots. Every event shows source attribution + click-through. Image provenance is stored; only small thumbnails for ingested content. DMCA takedown SLA is 48 hours.