AI-powered candidate screening for recruiters. Automate first-round interviews, get structured reports, and focus on the best fits.
MeritOS lets recruiters:
- Create roles — Paste a job description and optionally describe what to cover in plain English
- AI-generated interview plans — GPT-4o produces a structured question flow; recruiters can edit before deploying
- Share one link per role — Candidates complete the screening on their own schedule (voice or avatar mode)
- Review results — See completions and reports in the recruiter dashboard
flowchart TB
subgraph frontend ["Web App (Vite + React)"]
Landing[Landing Page]
Dash[Dashboard]
RoleNew[Create Role]
RoleDetail[Role Detail]
TemplateSetup[Template Editor]
CandEntry[Candidate Entry /i/:slug]
InterviewRoom[LiveKit Room]
SessionReview[Session Report]
end
subgraph convex ["Convex Backend"]
Roles[roles.*]
Templates[templates.*]
Sessions[sessions.* / sessionActions.*]
Candidates[candidates.*]
Reports[reports.* / reportHttp.*]
GenPlan[generatePlan]
HTTP[http.ts router]
end
subgraph agent ["Python Agent (LiveKit)"]
LiveKitAgent[Agent entrypoint]
FetchTemplate[GET template via HTTP]
Conduct[Conduct interview]
AvatarOrVoice[Voice-only or Avatar]
end
subgraph external [External Services]
OpenAI[OpenAI GPT-4o]
LiveKit[LiveKit Cloud]
ElevenLabs[ElevenLabs TTS]
Bey[Beyond Presence Avatar]
end
Dash --> Roles
RoleNew --> GenPlan
GenPlan --> OpenAI
RoleNew --> Templates
RoleDetail --> Sessions
TemplateSetup --> Templates
CandEntry --> Sessions
CandEntry --> InterviewRoom
InterviewRoom --> LiveKit
SessionReview --> Reports
LiveKitAgent --> LiveKit
LiveKitAgent --> FetchTemplate
FetchTemplate --> HTTP
HTTP --> Sessions
HTTP --> Templates
LiveKitAgent --> Conduct
Conduct --> AvatarOrVoice
AvatarOrVoice --> ElevenLabs
AvatarOrVoice --> Bey
upthena/
├── apps/
│ ├── web/ # Vite + React frontend
│ │ └── src/
│ │ ├── pages/ # Route pages (Dashboard, RoleNew, RoleDetail, etc.)
│ │ └── components/
│ │ └── Interview/InterviewRoom.tsx # LiveKit room component
│ └── agent/ # Python LiveKit agent (AI interviewer)
│ ├── main.py # Agent entrypoint
│ ├── convex_client.py # HTTP client for Convex
│ └── Dockerfile
├── packages/
│ └── db/ # Convex backend
│ └── convex/
│ ├── schema.ts # Data model
│ ├── roles.ts # Role CRUD
│ ├── templates.ts # Interview template CRUD
│ ├── sessions.ts # Session creation (candidate flow)
│ ├── sessionActions.ts # LiveKit room + token (Node action)
│ ├── sessionMutations.ts # Internal: mark started/completed
│ ├── candidates.ts # Candidate management
│ ├── reports.ts # Report queries
│ ├── reportHttp.ts # HTTP endpoint for agent report submission
│ ├── reportMutations.ts # Internal report creation
│ ├── interviewTemplateHttp.ts # HTTP endpoint for agent template fetch
│ ├── generatePlan.ts # GPT-4o interview plan generation
│ ├── http.ts # HTTP router
│ └── lib/devUser.ts # Dev-mode user helper
└── README.md
| Table | Purpose |
|---|---|
users |
Recruiter profiles (dev-mode single user for now) |
roles |
Job positions with shareable slug, status, optional deadline |
interviewTemplates |
Per-role config: sections, questions, persona, duration, voice/avatar mode |
candidates |
People who take interviews (name, email, status) |
interviewSessions |
LiveKit sessions linking role + candidate + template |
reports |
Post-interview summaries and scores |
You need three services running: Convex backend, web app, and the Python agent.
- Node.js 18+, pnpm (or npm)
- Python 3.11+
- Accounts: Convex, LiveKit Cloud, OpenAI, ElevenLabs
- Optional: Beyond Presence for avatar mode
cd packages/db
npm install
npx convex dev # starts local dev server, deploys schemaSet required environment variables in your Convex dashboard (or via CLI):
npx convex env set OPENAI_API_KEY "sk-..."
npx convex env set LIVEKIT_URL "wss://your-url.livekit.cloud"
npx convex env set LIVEKIT_API_KEY "your-key"
npx convex env set LIVEKIT_API_SECRET "your-secret"
# Optional: shared secret for agent report submission
npx convex env set CONVEX_AGENT_SECRET "some-secret"cd apps/web
cp .env.example .env
# Fill in: VITE_CONVEX_URL, VITE_LIVEKIT_URL
npm install
npm run devOpens at http://localhost:8080.
cd apps/agent
cp .env.example .env
# Fill in: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET,
# ELEVEN_API_KEY, ELEVEN_VOICE_ID, OPENAI_API_KEY,
# CONVEX_SITE_URL
# Optional: BEY_API_KEY, BEY_AVATAR_ID (for avatar mode)
pip install -r requirements.txt
python main.py dev- Go to http://localhost:8080/dashboard
- Click Add Role, paste a job description, generate the plan
- Edit the template if needed, then Activate the role
- Copy the candidate link (
/i/:slug) and open in a new tab - Enter name + email, click Start interview
- The AI agent conducts the screening (ensure the agent is running)
- Back in the dashboard, view the session under the role
| App | Variable | Required | Purpose |
|---|---|---|---|
| Convex | OPENAI_API_KEY |
Yes | Interview plan generation (GPT-4o) |
| Convex | LIVEKIT_URL |
Yes | LiveKit room creation |
| Convex | LIVEKIT_API_KEY |
Yes | LiveKit auth |
| Convex | LIVEKIT_API_SECRET |
Yes | LiveKit auth |
| Convex | CONVEX_AGENT_SECRET |
No | Shared secret for agent HTTP endpoints |
| Web | VITE_CONVEX_URL |
Yes | Convex client URL |
| Web | VITE_LIVEKIT_URL |
Yes | LiveKit WebSocket URL |
| Agent | LIVEKIT_URL |
Yes | LiveKit connection |
| Agent | LIVEKIT_API_KEY |
Yes | LiveKit auth |
| Agent | LIVEKIT_API_SECRET |
Yes | LiveKit auth |
| Agent | OPENAI_API_KEY |
Yes | STT + LLM (GPT-4o-mini) |
| Agent | ELEVEN_API_KEY |
Yes | ElevenLabs TTS |
| Agent | ELEVEN_VOICE_ID |
Yes | Voice selection |
| Agent | CONVEX_SITE_URL |
Yes | Convex HTTP API base |
| Agent | BEY_API_KEY |
No | Beyond Presence avatar |
| Agent | BEY_AVATAR_ID |
No | Avatar selection |
| Agent | CONVEX_AGENT_SECRET |
No | Must match Convex if set |
| Component | Options |
|---|---|
| Web | Vercel, Netlify, or any static host (npm run build) |
| Convex | npx convex deploy |
| Agent | Docker on Fly.io, Railway, or similar (see apps/agent/Dockerfile) |
This is an early version. The following are known constraints and decisions:
- No real auth. There is no Auth0/Clerk/OAuth integration. The recruiter dashboard uses a hardcoded dev user (
authSubject: "dev").ProtectedRouteis a passthrough. - Candidate flow is intentionally unauthenticated (name + email only).
- Templates are generated from a plain-text job description via GPT-4o. There is no option to upload PDFs, LinkedIn profiles, or structured job specs.
- The template editor is functional but minimal — section/question editing only, no drag-and-drop reordering or adding/removing sections from the UI.
- Reports are not yet generated by the agent. The infrastructure exists (HTTP endpoint, Convex mutations, report page), but the agent does not call
submit_report()after the interview. A placeholder report is created when the session ends. - The report page shows a summary field only — no per-section scores, transcript, or recording.
- The agent uses GPT-4o-mini for conversation and ElevenLabs for TTS. There is no pluggable model selection.
- Avatar mode requires a Beyond Presence account. Without it, the agent falls back to voice-only.
- The agent does not enforce a hard time limit — it relies on the LLM following the duration instruction.
- No ATS integration (Greenhouse, Lever, etc.). Candidates are tracked internally only.
- No email notifications. Recruiters must manually share the candidate link; candidates are not emailed.
- No recording/playback. Sessions are not recorded.
- Convex is used as the backend (not Supabase/Postgres). All data, mutations, and server-side logic live in Convex functions.
- There is no root
package.jsonor monorepo tooling (Turborepo, Nx). Each app is run independently. - The web app depends on
livekit-clientand@livekit/components-reactv2 for the interview room.
- Landing page, demo page, and sign-in page exist but are placeholders for a future marketing site.
- The dashboard is role-centric — there is no global candidate list or cross-role analytics.
- Dark mode is supported via CSS variables but there is no theme toggle in the UI.
Private — all rights reserved.