Skip to content

Case study · Metmee

Metmee. From a candidate’s camera to an assessment a recruiter can check.

Metmee runs first-round interviews without a calendar. A recruiter describes the role and shares one link; candidates record their answers when it suits them. Every answer is transcribed in any of around 40 languages, proofread, and scored by Claude against the recruiter’s own weights — with the reasoning written out, and the decision left to a person.

Client
Metmee · metmee.com
Product
Async video interviews, assessed by AI
Stack
Next.js 16 · React 19 · Supabase · Claude
Languages
Around 40
Built
31 May – 10 September 2026

Answer 2 of 401:48

Last year we moved a monolith to microservices. We deployed on Kubernetes, ran CI/CD through GitHub Actions, and watched it all in Grafana and Prometheus. The hardest part was splitting the database without any downtime.

Technical depth
84
Communication
76
Culture fit
70

77overall · weighted 40 / 30 / 30

Illustrative answer. The marked words are the kind the curated vocabulary exists for: the ones a general speech model gets wrong.
API routes
31
Tables under row-level security
8 of 8
Speech-to-text providers, one chain
3
Interview languages
~40

Counted from the Metmee repository at commit 7fb6448, 10 September 2026.

Station
The brief
Runs
The whole path

An MVP on paper. A product on a real phone.

The first requirements document asked for eleven things: a recruiter writes a job description, the AI suggests questions, candidates record video answers through a link, and the AI writes a qualitative assessment. What shipped is that product — rebuilt at each point where the MVP would have failed a real candidate, on a real connection, in a real language.

What the MVP requirements specified, and what shipped
AreaSpecified · 31 May 2026Shipped · 10 September 2026
AssessmentQualitative prose, without numeric scores.Scores for technical depth, communication and culture fit, per question and overall — the overall computed from the recruiter’s weights, not by the model.
TranscriptionResponses are transcribed and sent for assessment.Transcribed on the server for every browser: three providers in a chain, a vocabulary curated per interview, and a proofreading pass that is not allowed to invent.
RecordingCamera and microphone through the MediaRecorder API.Camera, or audio only when the camera fails; five recording formats tried in order; the bitrate capped so a phone can upload what it records.
AccessRow-level security: each recruiter sees only their own interviews.The same, plus a secret per candidate session on every write, rate limits by route, and approval before a new recruiter account can use the product.
TimingA 30-second timeout on AI requests.A budget per job: 20 s for a follow-up, 30 s for question suggestions, 60 s per clip, 90 s for an assessment, 300 s for everything after a candidate submits.
QuestionsA fixed set, suggested by AI and edited by the recruiter.The same — or an adaptive interview, which can ask up to three follow-ups per question, written from what the candidate just said.
LanguageNot specified.Interviews, transcription and assessment in around 40 languages.
Station
Capture
Runs
The candidate’s browser
Budget
300 s per question, by default

Every browser records something.

The first station is the one with the least control: somebody else’s phone, somebody else’s connection, a browser that says it supports a format it does not. So nothing here assumes. Each choice is a ladder, tried from the top, with a floor that still lets the interview finish.

Camera and microphone

  1. Front camera, 1280 × 720Mono audio with echo cancellation, noise suppression and automatic gain
  2. Front camera, any resolution
  3. Any camera, any microphone

Floor Audio only The interview still completes. A recruiter can also set an interview to audio from the start.

Recording format

  1. WebM · VP9 + Opus
  2. WebM · VP8 + Opus
  3. WebM
  4. MP4 · H.264 + AACSafari and iOS
  5. MP4

Floor The browser’s own default Used outright on OEM Android browsers, which report support they do not have.

MB per minute of answer

  • Safari on iOS, left to its defaults 4–8 Mbit/s30–60 MB
  • Metmee, capped 1.2 Mbit/s video · 128 kbit/s audio≈ 10 MB
Worked out from the bitrates: Safari on iOS left to its 4–8 Mbit/s default is 30–60 MB a minute; Metmee’s cap of 1.2 Mbit/s video and 128 kbit/s audio is (1.2 + 0.128) × 60 ÷ 8 ≈ 10 MB.

Upload, bytes sent over time

45 s, no bytes
  • Slow, but moving — finishes
  • Stalled — abandoned after 45 s without progress
  • The next attempt, on a new signed URL
Schematic. The watchdog is reset every time bytes move. A slow connection that keeps moving finishes; one that stops for 45 seconds is abandoned, and the next attempt starts on a new signed URL.
  1. Bitrate capped at 1.2 Mbps

    Safari on iOS records at 4–8 Mbps by default — 30 to 60 MB for every minute of talking head, which then fails to upload on a phone. 1.2 Mbps is plenty for 720p.

  2. Straight to storage

    Each clip goes from the browser to a private bucket on a signed URL. It never passes through the application server.

  3. Stalled, not slow

    An upload is abandoned only when no bytes have moved for 45 seconds. A slow connection that keeps moving is allowed to finish.

  4. A fresh URL for every retry

    Signed upload tokens are single-use, so each of the three retries asks for a new one before it starts.

  5. Proctoring that knows about permission prompts

    Leaving the tab is logged against the question. The browser’s own camera dialog is not — and neither is the second and a half after it closes.

  6. A clock per question

    Five minutes by default, set per question, with an optional limit on the interview as a whole.

Station
Transcription
Runs
Server, after submit
Budget
60 s per clip · all clips in parallel

Two passes, and the second one is not allowed to invent.

A candidate for a technical role says “we deployed on Kubernetes and watched it in Grafana” in one breath — and when they speak another language, the English terms arrive in the middle of it. A general speech model mangles exactly those words. Metmee transcribes in two passes: a speech model that has been told which words to listen for, then a proofreader that may only fix what was misheard.

  1. Collect

    Words from the job title, the description and the questions

  2. Curate

    Claude Haiku keeps the 15–40 a recogniser would miss. Once per interview, shared by every clip

  3. Hear

    Speech-to-text, routed by language and length. Three providers, tried in order

  4. Proofread

    Claude, temperature 0. Spelling, jargon and punctuation only

  5. Check

    Length must stay between 0.5× and 1.6× of what was heard. Otherwise the raw transcript stands

Which speech model hears an answer, by the interview’s language and the clip’s length
AnswerModelHow it listens
EnglishAssemblyAI Universal-3.5 ProA short prompt naming the role and the question, plus the curated terms.
Other languages — under 20 sAssemblyAI universal-2Fixed to the interview’s language: detection misfires on clips this short.
Other languages — 20 s and longerAssemblyAI universal-2Language detection with code-switching on — so English terms inside another language are heard as English.

If a provider fails, or hears nothing

  1. AssemblyAI Universal-3.5 Pro · universal-2
  2. OpenAI gpt-4o-transcribe
  3. Deepgram nova-2
Corrected length ÷ heard length: kept between 0.5× and 1.6×. A faithful proofread fixes spelling and punctuation; it does not make a transcript 60% longer, or half as long. Outside the band the correction is thrown away. And the proofreader never sees the question — given it, a model rewrites a garbled answer into one that fits, which is the one thing an evaluation tool must not do.
lib/services/transcription.ts
const rawLen = rawText.trim().length;
const corrLen = corrected.length;
if (rawLen > 0 && (corrLen > rawLen * 1.6 || corrLen < rawLen * 0.5)) {
    console.warn(
        `[transcription] correction discarded (length drift raw=${rawLen} corrected=${corrLen}); keeping raw`
    );
    return null;
}
  1. Line 3 The band, as the code states it.
  2. Line 7 Returning nothing keeps the raw transcript — what the candidate actually said.
Station
Scoring
Runs
Server, after transcription
Budget
90 s · 3,000 tokens

The model scores three things. It does not score the whole.

Claude returns a score from 0 to 100 for technical depth, communication and culture fit, for each question and overall, with the reasoning written out. The overall that ranks one candidate above another is not the model’s number: it is the recruiter’s weights, applied the same way every time — which is what makes a ranking something a person can explain.

The recruiter’s weights
40
30
30

Sum 100. The weighted sum is divided by it, so the weights are proportions, not points.

Ranking · illustrative candidates

  1. Candidate BTechnical depth 70 · Communication 80 · Culture fit 82Overall 77
  2. Candidate ATechnical depth 88 · Communication 62 · Culture fit 58Overall 71

Each candidate’s three scores stay fixed — as the model returned them. Only the weights move.

lib/anthropic.ts
// Deterministic weighted overall — keeps ranking explainable & stable.
const { technical, communication, culture_fit } = input.weights;
const totalWeight = technical + communication + culture_fit || 1;
const overallScore = Math.round(
    (technicalScore * technical +
        communicationScore * communication +
        cultureFitScore * culture_fit) /
    totalWeight
);
  1. Line 2 The recruiter’s weights — 40 / 30 / 30 unless they change them.
  2. Line 3 An interview with every weight at zero does not divide by nothing.
  3. Line 4 The same arithmetic for every candidate, so a ranking can be re-run and explained.

Five levels of strictness, set per interview.

Strictness is written into the prompt as explicit score ranges, so “strict” means the same thing on every assessment. The same generic answer is worth 60–70 at the first level and 20–40 at the fifth.

Where a generic answer and a weak answer land at each of the five strictness levels, out of 100
LevelGeneric answerWeak or emptyOn a scale of 0 to 100
1 Lenient — entry-level roles60703045
2 Moderately lenient55652540
3 Balanced — the default45601535
4 Strict3550520
5 Very strict — senior roles2040015
  1. Judged on content, not on the transcript

    The prompt tells the model the transcript is machine-made: filler words, repetitions and recognition errors are not held against the candidate.

  2. Red flags only when they are there

    Contradictions between answers, claims that do not hold together, answers that sound memorised or evasive — and an empty field when there are none. The prompt says, in capitals, not to invent them; the interface calls them “points to verify”.

  3. A cut-off answer is still an answer

    When a long interview runs the model into its token limit, the truncated JSON is repaired — the open string closed, the dangling comma dropped, the brackets balanced — instead of failing with a 500.

  4. Retries for rate limits only

    A rate-limited call waits 2, 4, then 8 seconds and tries again. Anything else fails, marks the assessment failed, and puts a retry button in front of the recruiter.

Station
Follow-up
Runs
Server, between two questions
Budget
20 s · 256 tokens

One more question, when it is worth asking.

In an adaptive interview, each answer is read between one question and the next, and the model decides whether a single follow-up would tell the recruiter something the answer did not. The right answer is often no, and the prompt spends as many lines on when not to ask as on when to.

Ask when

  • The candidate mentioned something specific and did not go into it.
  • The answer is vague, and a probe could show whether there is depth behind it.
  • There is a claim worth checking, or a contradiction worth clearing up.

Do not ask when

  • The answer is already thorough and concrete.
  • The answer is empty, off-topic or makes no sense — a follow-up will not help.
  • The follow-up would only be a generic “can you say more”.
  1. One question

    About 25 words at most, built on what the candidate actually said.

  2. Up to three per question

    Set per interview, between none and three; one by default.

  3. Only when there is an answer

    Under eight characters of transcript, the model is not called at all.

  4. Asked of one candidate

    The follow-up is stored against that session alone, so no one else is ever asked it.

Station
Review
Runs
The recruiter’s dashboard
Budget
polls every 5 s until the assessment lands

What the recruiter gets, and what they decide.

Everything upstream arrives as one assessment, in a shape the model is not free to change: the prompt names every field it must return, and the product checks them on the way in. The overall is not among them — the product adds it. The decision is the one thing on the page the software does not make.

What the model must return — and what the product adds

strengths string
A paragraph, with examples from the answers
weaknesses string
Areas for improvement, specific to the role
recommendation string
The recommendation, and why
recommendation_level strong_yes | yes | maybe | no
Checked against the four; otherwise derived from the overall
red_flags string
Shown as “Points to verify”, or left empty
technical_score 0–100
Rounded and clamped
communication_score 0–100
Rounded and clamped
culture_fit_score 0–100
Rounded and clamped
per_question [{ index, score, comment }]
One per question, three or four sentences each
overall_score 0–100
Computed from the recruiter’s weights · added by the product
Illustrative. Each region of the card is labelled with the field it is drawn from.

Pace, words per minute

Under 110 reads as slow, over 160 as fast, and between them as natural — the interface’s own words.

Speech, measured without a model

Pace
Words per minute over the recorded duration, read against the band the interface calls natural.
Filler words
Twelve per language, phrases counted before single words so nothing is counted twice.
Vocabulary
Unique words over total words — how varied the answer is.
Sentence length
Average words per sentence — how the answer is built.
The four levels the model may return, and what the interface calls them. The thresholds apply only when the level has to be derived from the overall.
LevelShown asDerived when
strong_yesStrongly recommend80 and over
yesRecommend65 to 79
maybeConsider45 to 64
noDo not recommendUnder 45

The four levels the model may return, and what the interface calls them. The thresholds apply only when the level has to be derived from the overall.

  1. No model involved

    Every speech metric is a count or a ratio from the transcript: free, instant, reproducible — and it never touches the score.

  2. A decision, then a letter

    Accepting or rejecting a candidate records the decision and tells the candidate by email.

  3. Video on a one-hour link

    Answers play from a private bucket on a signed URL that expires after an hour.

  4. Comparison and PDF

    Every candidate for a role on one table, and any single assessment as an A4 PDF.

Station
Hardening
Runs
Every request

Candidates have no account. Their writes still need a key.

A recruiter signs in, and row-level security keeps them inside their own interviews — on all eight tables. A candidate does not sign in at all, so every candidate write passes its own gates, in this order, before anything reaches the database. Responses carry HSTS, a policy that forbids framing, and a connect list that lets the browser talk to the app, Supabase and Anthropic, and nothing else.

  1. Rate limitBy route and client
  2. SchemaEvery body parsed by Zod before any code reads it
  3. Session secretThe x-session-secret header must match the one issued with the session
  4. WriteOnly then, with the service role
Rate limits, by scope
ScopeWindowRequests
Authentication60 s10
New candidate sessions60 s20
AI question suggestions60 s15
Everything else60 s60

Found, and closed on 5 June

Candidate routes run with the service role, and at first they trusted the session ID alone: anyone who had a session’s ID could write to it. A secret issued with each session, and required on every candidate write, closed it. Sessions created before the change hold no secret — and a missing secret never matches, so nobody can write to them.

Known, and written down

The rate limiter keeps its counts in memory, per server instance, in fixed one-minute windows. It stops bursts and brute force; it is not a global limit across instances. The project’s README says so, and names the fix: back it with Redis.

Station
Timeline
Runs
Git history

Fifty-nine commits, three waves.

The core and the production pass landed in the first week of June. The second wave, in July, added what the first month of use asked for. The third, in September, went back to the hardest station and rebuilt it.

  1. Requirements

    • Eleven requirements for an MVP, written before the product code
  2. The core

    • Schema with row-level security
    • The candidate link, session and recording
    • Proctoring
    • The results page
  3. The production pass

    • Numeric scoring and weights
    • Candidate comparison
    • Server-side transcription
    • Uploads that survive a phone
    • Rate limits and security headers
    • Email
  4. Localisation

    • Every string of the interface moved into dictionaries, so the product can change language
  5. The public site

    • The product, how it works, and pricing
  6. Decisions, and a secret

    • Accept, reject and notes
    • A secret per candidate session
    • Company branding on candidate pages
  7. Red flags

    • Contradictions and evasive answers, flagged for a person
  8. The second wave

    • Adaptive follow-up questions
    • Speech analytics
    • Approval for new accounts
    • Outreach: a company’s site summarised, an email drafted, a person edits and sends
  9. Transcription, rebuilt

    • A vocabulary curated per interview
    • English on Universal-3.5 Pro
    • Code-switching for everything else
    • Proofreading behind a fabrication guard
Commits by month, 2026
  1. 1May
  2. 54Jun
  3. 3Jul
  4. 0Aug
  5. 1Sep
Station
Spec sheet
Runs
Repository
Budget
commit 7fb6448

The product, counted.

Every number on this page, and the rest of them, from one reading of the repository.

Code

TypeScript files
137
Lines of TypeScript
16,906
API route handlers
31
HTTP methods
40
Pages
19
Services · client hooks
9 · 4

Data

Tables
8
Under row-level security
8 of 8
Policies
16
Migrations
12
CHECK constraints
27
Storage buckets
2

AI

Claude prompts
6
Assessment model
Sonnet 4.6
Vocabulary model
Haiku 4.5
Speech-to-text providers
3
Strictness levels
5
Follow-ups per question
0–3

Budgets

Follow-up
20 s
Question suggestions
30 s
Transcription, per clip
60 s
Assessment
90 s
After submit
300 s
Upload stall
45 s

Surface

Interview languages
~40
Request schemas (Zod)
16
Email templates
5
Proctoring event types
5
Invites per send
200
Recording formats tried
5

Have a similar challenge?

Tell us what you want to change. We’ll discuss the work that would support it.

Discuss your project

Balkaris®Technology meets human potentialOther capabilities

Recommended for you

Since you’re exploring Digital, you might also need this.

Combine technology with growth and creative execution to unlock even bigger results — from strategy to scale, all in one place.

Digital / Explore / What’s nextDifferent expertise. A stronger tomorrow.