Your always-on job application relay station. Discovers jobs, tailors your resume (zero fabrication), handles form submission, and tracks every application through its lifecycle.
Relay is a locally-run, autonomous job application system with a web dashboard. It:
- Discovers job listings from LinkedIn, Indeed, and company sites
- Parses JDs with Claude to extract required skills, role level, culture signals
- Scores your resume against each JD (0–100 match score)
- Tailors your resume — selecting and reordering bullets by relevance, injecting JD keywords (zero fabrication enforced at prompt + verification layers)
- Applies via browser automation (Playwright) with anti-detection stealth
- Escalates any field it can't answer with ≥ 0.85 confidence to a human review queue
- Tracks every application through the full lifecycle on a Kanban dashboard
graph TB
subgraph Oracle Cloud VM ["☁️ Oracle Cloud ARM VM (Always Free)"]
API[FastAPI Backend :8000]
DB[(SQLite + WAL)]
SCHED[APScheduler]
REACT[React Dashboard\nServed as Static]
SCHED -->|every 6h| DISC[Discovery Pipeline]
SCHED -->|every 5min| DISPATCH[Dispatch Check]
DISC --> DB
DISPATCH --> DB
API <--> DB
REACT --> API
end
subgraph Local Machine ["💻 Your Local Machine (Residential IP)"]
AGENT[local_agent.py\nPolls every 5min]
PW[Playwright Browser]
AGENT --> PW
end
subgraph Platforms ["🌐 Job Platforms"]
LI[LinkedIn Easy Apply]
IN[Indeed Quick Apply]
GH[Greenhouse / Lever]
WD[Workday]
end
subgraph LLM ["🤖 Anthropic Claude"]
JDP[JD Parser]
MS[Match Scorer]
BS[Bullet Selector]
KI[Keyword Injector]
end
AGENT -->|POST /api/automation/claim| API
PW --> LI & IN & GH & WD
AGENT -->|POST /api/automation/complete| API
API <--> LLM
style Oracle Cloud VM fill:#f0f7ff,stroke:#2563eb
style Local Machine fill:#f0fdf4,stroke:#16a34a
style LLM fill:#fef9f0,stroke:#d97706
flowchart LR
A[🔍 Scrapers] -->|raw job listings| B[Deduplicator\nhash-based]
B -->|new jobs only| C[Red Flag Filter\nexcluded companies\nvisa requirements]
C -->|clean listings| D[JD Parser\nClaude LLM]
D -->|ParsedJD| E[Match Scorer\nkeyword + semantic]
E -->|score ≥ 65| F[App Queue\nSQLite priority queue]
E -->|score < 65| X1[❌ Skipped]
F -->|dequeued| G[Bullet Selector\nper-bullet LLM scoring]
G --> H[Keyword Injector\nno-fabrication enforced]
H --> I[Resume Renderer\nWeasyPrint PDF]
I --> J[Confidence Scorer\nper field decision]
J -->|≥ 0.85 confidence| K[Platform Handler\nPlaywright]
J -->|< 0.85 required| L[⏳ Pending Queue\nhuman review]
L -->|resolved| K
K -->|success| M[✅ Applied\nStage tracking]
K -->|error| N[Assisted Mode\nvisible browser]
K -->|crash| O[❌ Failed → retry]
style A fill:#dbeafe
style F fill:#dcfce7
style L fill:#fef9c3
style M fill:#d1fae5
style X1 fill:#fee2e2
style O fill:#fee2e2
stateDiagram-v2
[*] --> DISCOVERED: job found by scraper
DISCOVERED --> QUEUED: score ≥ 65, no red flags
DISCOVERED --> SKIPPED: red flag / low score
DISCOVERED --> EXPIRED: stale after 14 days
QUEUED --> TAILORING: dequeued for dispatch
QUEUED --> SKIPPED: manual skip
QUEUED --> EXPIRED: stale after 14 days
TAILORING --> APPLYING: all fields resolved
TAILORING --> PENDING_CLARIFICATION: required field < 0.85 confidence
TAILORING --> FAILED: LLM error / render failure
PENDING_CLARIFICATION --> APPLYING: human resolves fields
PENDING_CLARIFICATION --> SKIPPED: human rejects app
APPLYING --> APPLIED: confirmation detected
APPLYING --> FAILED: handler error
FAILED --> QUEUED: manual retry
APPLIED --> APPLIED: stage updates\n(Screening / OA / Phone\n/ Interview / Offer\n/ Rejected / Ghosted)
erDiagram
applications {
text id PK
text company
text role_title
text source_url
text source_platform
text jd_hash FK
text resume_version_id FK
text status
text stage
real match_score
timestamp applied_at
timestamp created_at
}
stage_history {
int id PK
text application_id FK
text from_stage
text to_stage
text from_status
text to_status
timestamp changed_at
text changed_by
}
jd_cache {
text content_hash PK
text raw_text
text parsed_json
real parse_confidence
timestamp created_at
}
resume_versions {
text id PK
text application_id FK
text tailored_json
text pdf_path
text render_hash
timestamp created_at
}
answer_bank {
int id PK
text key
text value
text format_hint
text country_tag
int usage_count
}
pending_clarifications {
int id PK
text application_id FK
text field_name
text suggested_answer
real confidence
text status
}
seen_hashes {
text content_hash PK
text company
text role_title
timestamp first_seen
}
applications ||--o{ stage_history : "has history"
applications ||--o| jd_cache : "references"
applications ||--o| resume_versions : "uses"
applications ||--o{ pending_clarifications : "has"
gantt
title Relay — Build Phases
dateFormat YYYY-MM-DD
section Foundation
Phase 1 · Data Foundation :active, p1, 2026-05-24, 2d
Phase 2 · LLM Pipeline :p2, after p1, 3d
Phase 3 · Answer Bank :p3, after p2, 2d
Phase 4 · Queue + State Machine :p4, after p3, 2d
section Automation
Phase 5 · Browser Automation :p5, after p4, 4d
Phase 6 · Scrapers :p6, after p5, 3d
section Frontend
Phase 7 · Dashboard :p7, after p6, 4d
section Hardening
Phase 8 · Integration :p8, after p7, 2d
| Layer | Tech |
|---|---|
| Backend | Python 3.11, FastAPI, SQLAlchemy async |
| Database | SQLite + WAL mode via aiosqlite |
| LLM | Anthropic Claude (claude-sonnet-4-20250514) |
| Browser | Playwright async + playwright-stealth |
| Resume | WeasyPrint (HTML→PDF) + python-docx fallback |
| Frontend | React 18, Vite, TailwindCSS, Zustand, TanStack Query |
| Scheduling | APScheduler (AsyncIOScheduler) |
| Deployment | Oracle Cloud ARM Always Free |
# Clone and set up
git clone https://github.com/AryanG01/relay.git
cd relay
# Python environment
python3.11 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
playwright install chromium
# Configure secrets
cp .env.example .env
# Edit .env: add ANTHROPIC_API_KEY and SECRET_KEY
# Initialize database + seed answer bank
python scripts/setup.py
# Start backend
uvicorn backend.main:app --reload --port 8000
# In another terminal — start frontend (dev)
cd frontend && npm install && npm run devrelay/
├── backend/
│ ├── main.py # FastAPI app entry point
│ ├── config.py # Pydantic Settings
│ ├── database.py # Async SQLAlchemy engine
│ ├── models.py # ORM models
│ ├── schemas.py # Pydantic schemas
│ ├── services/ # Business logic
│ ├── automation/ # Playwright handlers
│ ├── scrapers/ # Job discovery
│ ├── routers/ # FastAPI route handlers
│ └── utils/ # LLM client, hashing, logging
├── frontend/ # React + Vite dashboard
├── data/ # SQLite DB, resume JSON, config (gitignored)
├── templates/ # Resume HTML template
├── scripts/ # setup.py, local_agent.py, test_pipeline.py
├── deploy/ # systemd service, nginx config, setup.sh
├── plan/ # Implementation plans (current phase)
└── tests/ # pytest test suite
Runs perpetually at ~$0/month on Oracle Cloud Always Free (ARM Ampere A1 — 4 cores, 24GB RAM).
Playwright form submission runs on your local machine (residential IP) to avoid datacenter detection.
See deploy/ for systemd service, nginx config, and one-shot setup script.
- Zero fabrication — keyword injector may rephrase bullets but never introduces new claims; enforced at prompt + post-injection verification layers
- Human-in-the-loop — fields below 0.85 confidence route to Pending queue; partial applications never submitted
- Graceful degradation — every handler falls back to assisted mode (visible browser + clipboard pre-load)
- Idempotent — re-running any pipeline stage on the same input produces the same output
- Full audit trail — every state transition, form fill, and LLM call logged with timestamp