FirstIn
Poll company career pages directly. Get notified before the listing spreads.
FirstIn monitors ATS career APIs on a configurable schedule, filters postings by title and location keywords, deduplicates against a local SQLite store, and sends alerts via Slack or stdout when new matches appear.
It targets source APIs directly - Greenhouse, Ashby, Lever, and Workday, rather than job aggregators. Each polling cycle fetches fresh listings, applies freshness and keyword filters, and only notifies on jobs it has not seen before.
On first run, existing listings are silently seeded into the store. Subsequent runs alert only on genuinely new postings.
❗ Note on polling behavior: FirstIn targets public ATS APIs that companies intentionally expose for career page integrations. It does not scrape HTML, bypass authentication, or simulate browsers. Requests are spaced by a configurable minimum delay (default 10 minutes between requests to any given ATS).
Architecture
flowchart LR
A[Scheduler] --> B[CompanyPoller]
B --> C[ATS Adapter]
C --> D[RetryFetcher]
D --> E[Title / Location Filter]
E --> F[Freshness Check]
F --> G[SQLite Dedup Store]
G --> H[Notifier]
▸ Scheduler — groups companies by ATS, runs one goroutine per group with structural rate limiting between requests
▸ CompanyPoller — orchestrates a single poll cycle: fetch, filter, dedup, notify, mark seen
▸ ATS Adapter — normalizes each platform's API response into a unified Job struct
▸ RetryFetcher — decorator wrapping any adapter; exponential backoff with jitter on transient failures
▸ SQLite Store — lightweight dedup layer; tracks seen job IDs with a first_seen timestamp
▸ Notifier — Slack (Block Kit) or structured log output
All components are wired through interfaces. main() is the only site that references concrete types.
Features
| Capability |
Description |
| Multi-ATS support |
Greenhouse, Ashby, Lever, and Workday adapters included |
| Keyword filtering |
Case-insensitive substring matching on title and location, with include and exclude lists |
| Freshness gating |
Jobs older than max_age (default 24h) are skipped after the initial seed run |
| Deduplication |
SQLite-backed seen-jobs store; each job ID is persisted on first encounter |
| Retry with backoff |
Exponential backoff with ±30% jitter; respects Retry-After on HTTP 429 |
| Rate limiting |
Configurable minimum delay between requests to the same ATS (default 10m) |
| Slack notifications |
Block Kit messages with apply button; flood-protected with per-message delay |
| TUI audit browser |
Interactive split-pane viewer to browse and inspect live job listings |
| Dry-run mode |
One-shot poll with no writes to the store; useful for testing filters |
| Single binary |
No runtime dependencies; runs on Linux, macOS, or Docker |
FirstIn TUI