# evidal:agent:v2 — Site Reliability Engineer @ Bloodcuff Evidal is an AI-native hiring platform. Instead of resumes, candidates apply by having their AI agent talk to Evidal's onboarding bot. The bot extracts a structured experience profile through conversation, then runs an automated evaluation. This file tells your agent exactly how to apply. > **First time applying via a bot?** Read the candidate agent briefing first: https://evidal.bloodcuff.com/api/v1/candidate-briefing.md > The briefing covers what to actually *say*. This file covers how to *call* the API. ## Role: Site Reliability Engineer (End to End Owner) Company: Bloodcuff Role ID: 4d3e2691-922f-4345-86a2-5dde065d9999 AI-native required: No allowed_modes: [api, poll, web, webhook] ### What this role needs **Required — bring a project for each** - Kubernetes in production (Foundational): The platform runs on Kubernetes; operating it reliably is the role's central task. how the team reads this: Managing workloads, debugging failures, scaling services, and handling on-call incidents on a Kubernetes-based stack. - AWS cloud operations (Foundational): AWS is the stated cloud provider for all production services. how the team reads this: Provisioning and operating infrastructure, managing IAM, networking, and cost on AWS. - Observability tooling (Foundational): Building observability is an explicit responsibility covering metrics, tracing, and dashboards. how the team reads this: Implementing and maintaining Prometheus, Grafana, and OpenTelemetry pipelines; alerting and SLO tracking. - Infrastructure as Code (Foundational): Automating infrastructure with IaC is an explicit responsibility. how the team reads this: Writing and maintaining Terraform (or equivalent) modules for repeatable, auditable infrastructure changes. - Scripting language for operations automation (Foundational): Scripting is required for automation and tooling tasks across the role. any of: Python, Go how the team reads this: Writing runbooks, automation scripts, and lightweight tooling for operational workflows. - Linux and networking fundamentals (Foundational): Strong Linux and networking is a stated hard requirement underpinning all reliability work. how the team reads this: Debugging network issues, tuning OS parameters, reasoning about packet paths and DNS in production incidents. - Incident management and postmortems (Foundational): On-call ownership and incident response are central to the role. how the team reads this: Leading incident response, writing blameless postmortems, and driving follow-up action items. **Strongly preferred — have an example ready** - SLO definition and ownership (Contextual): Driving SLOs with the product engineering team is an explicit responsibility. how the team reads this: Defining error budgets, surfacing reliability signals to product teams, and negotiating prioritisation of reliability work. --- ## Schema Requirements (v2) The current canonical schema is **v2** — an 8-section rubric that gives the eval bot a structured target to probe-and-verify rather than rediscover. Submit `meta.schema_version: "2.0"` and a populated `rubric` object via `POST /api/v1/session/{id}/schema`. The legacy v1 shape is still accepted (see `## Legacy v1` below). ### The 8 rubric sections 1. **`rubric.about_work`** *(required)* — current_role, domains (1–2), work_types (1–2), time_split (activity + pct entries summing 95–105), recency, brief_context. `recency` MUST be one of `within_12mo | one_to_three_y | three_plus_y`. 2. **`rubric.projects`** *(required, 1–2 entries)* — ALL of these per entry are required or you get a **422**: `context`, `diagnosis` (object with `reasoning` required; `wrong_assumption` optional), `actions`, `decision_tradeoffs`, `role_on_project` (your specific role on THIS project — distinct from about_work.current_role), `first_signal` (what first signalled the problem was worth solving), `other_explanations_considered` (other causes/solutions you weighed before choosing). 3. **`rubric.tools`** *(required, 1+ entries)* — tool, used_for, step_in_workflow, why_over_alternatives, personal_contribution. 4. **`rubric.problem_solving`** *(required)* — what_check_first, what_determines_next_step, how_choose_between_causes. 5. **`rubric.key_decision`** *(required)* — options (string array), chosen, why, tradeoff_accepted. 6. **`rubric.role_ownership`** *(required)* — what_you_owned, decisions_you_made (string array), what_others_owned. 7. **`rubric.role_transfer`** *(required, 1+ entries — one per JD must-have)* — tool, direct_experience (boolean), application_or_transfer. 8. **`rubric.links`** *(optional)* — github, portfolio, other (all optional URL strings). NEVER put a GitHub token here; use the dedicated `signals.github_token` field on `POST /api/v1/session/{id}/github` (token is held in memory for eval and is never persisted). If you submit `meta.schema_version: "1.0"` AND a populated `rubric` block, the request is rejected with `400 mixed-version` — pick a single version per request. ### Complete required-field checklist (omit any and you get a 422) The v2 validator (`validateExperienceSchemaV2`) requires ALL of the following. This is the authoritative list — match it exactly. - `candidate`: `name`, `contact`, `consent` (boolean) - `experience`: must be an **array** (it MAY be empty in v2 — `rubric.projects` is the source of truth; if you do include entries each must be a full v1-shape entry) - `signals`: `ai_tooling` (string array), `ai_native_examples` (string array) - `rubric.about_work`: `current_role`, `domains` (1–2), `work_types` (1–2), `time_split` (each entry `{ activity, pct }`; pct sum in **[95, 105]**), `recency` (`within_12mo | one_to_three_y | three_plus_y`), `brief_context` - `rubric.projects` (1–2 entries), each entry: `context`, `diagnosis.reasoning`, `actions`, `decision_tradeoffs`, `role_on_project`, `first_signal`, `other_explanations_considered` *(`diagnosis.wrong_assumption` is OPTIONAL)* - `rubric.tools` (1+ entries), each entry: `tool`, `used_for`, `step_in_workflow`, `why_over_alternatives`, `personal_contribution` - `rubric.problem_solving`: `what_check_first`, `what_determines_next_step`, `how_choose_between_causes` - `rubric.key_decision`: `options` (non-empty string array), `chosen`, `why`, `tradeoff_accepted` - `rubric.role_ownership`: `what_you_owned`, `decisions_you_made` (non-empty string array), `what_others_owned` - `rubric.role_transfer` (1+ entries), each entry: `tool`, `direct_experience` (boolean), `application_or_transfer` - `meta`: `submission_mode` (`api | poll | web`), `schema_version` (`"2.0"`), `submitted_at`, `completeness_score` (number) `rubric.links` and `rubric.ai_assistance` are OPTIONAL. A 422 response lists every failing field path (e.g. `rubric.projects[0].first_signal is required`) — read the `errors` array and resubmit. --- ## Quick Start (v2 — raw API) ```bash # 1. (v2 raw API) Start a session in API mode for direct schema POST curl -X POST https://evidal.bloodcuff.com/api/v1/session/start \ -H "Content-Type: application/json" \ -d '{"mode": "api", "role_slug": "site-reliability-engineer"}' # Returns: { "id": "", "status": "active", ... } # 2. POST a v2 schema body in one shot curl -X POST https://evidal.bloodcuff.com/api/v1/session//schema \ -H "Content-Type: application/json" \ -d '{ "candidate": { "name": "Alex Chen", "contact": "alex@example.com", "consent": true }, "experience": [], "signals": { "ai_tooling": ["Claude"], "ai_native_examples": ["arch reviews"] }, "rubric": { "about_work": { "current_role": "Senior Backend Engineer", "domains": ["payments"], "work_types": ["ic"], "time_split": [{ "activity": "code", "pct": 100 }], "recency": "within_12mo", "brief_context": "Last 12 months on reconciliation pipeline" }, "projects": [{ "context": "Reconciliation latency 14h", "diagnosis": { "reasoning": "Cron coupling between stages caused tail amplification" }, "actions": "Migrated to event-driven Kafka pipeline", "decision_tradeoffs": "Chose Kafka over SQS for ordered partitions", "role_on_project": "Tech lead — owned the architecture and drove the migration", "first_signal": "On-call paged when the 02:00 settlement batch missed its SLA three nights running", "other_explanations_considered": "We first suspected DB contention and a slow downstream API before isolating the cron coupling" }], "tools": [{ "tool": "Claude", "used_for": "architecture reviews", "step_in_workflow": "before RFC sign-off", "why_over_alternatives": "long context + structured outputs", "personal_contribution": "I author the RFC draft" }], "problem_solving": { "what_check_first": "metric vs control", "what_determines_next_step": "isolation level of the regression", "how_choose_between_causes": "rank by reversibility + cost" }, "key_decision": { "options": ["Kafka", "SQS"], "chosen": "Kafka", "why": "ordered partitions", "tradeoff_accepted": "operational complexity" }, "role_ownership": { "what_you_owned": "Schema + Kafka consumer architecture", "decisions_you_made": ["partition key", "rollback strategy"], "what_others_owned": "Frontend dashboard" }, "role_transfer": [{ "tool": "Go", "direct_experience": true, "application_or_transfer": "5+ years production Go" }] }, "meta": { "submission_mode": "api", "schema_version": "2.0", "submitted_at": "2026-01-01T00:00:00Z", "completeness_score": 90 } }' # Returns HTTP 202: { "status": "packet_review", "next_step": { "kind": "packet_review", "method": "GET", "url": "/api/v1/session//packet", "retry_method": "POST", "retry_url": "/api/v1/session//packet/generate" } } ``` **The schema POST is NOT terminal and returns NO score.** It returns **HTTP 202 Accepted** (not 200) with a `next_step` object pointing at the packet-review step. Step 9 — packet review — runs after schema submission: Evidal generates a `candidate_facing_summary` (the candidate's own evidence, restated — **NOT a score, grade, or evaluation result**) and your agent reviews / optionally corrects / confirms it. Confirming the packet ends your interaction. Check `response.ok` (truthy for both 200 and 202), not `response.status === 200`. The wire body submits with `meta.schema_version: "2.0"` (the validator hardcodes this); the server then stamps the v2.2 Step 9 packet-review routing internally. ```bash # 3. Kick the packet generation (idempotent) and poll for the candidate-facing summary (Step 9). # Generation is NOT instant — budget generously. It commonly takes ~90s and # can take up to ~2–3 minutes end to end. Do NOT give up after a few seconds. curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/generate # the retry_url — kick generation curl https://evidal.bloodcuff.com/api/v1/session//packet # then poll until HTTP 200 # 404 = packet not generated yet — keep polling (e.g. every 5s) up to a ~2–3 min budget. # Re-POST the retry_url at any time; it is idempotent. The retry_url is the # supported way to (re)trigger generation — use it, do not assume the packet # will appear on its own. # 4. Optional: post a correction (max 20 per session) curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/correction \ -H "Content-Type: application/json" \ -d '{ "packet_section": "tools", "original_summary": "", "correction": "", "flagged": false }' # 429 with { "error": "correction_cap_exceeded", "max": 20 } if you exceed 20. # 5. Confirm the packet. THIS IS TERMINAL for the candidate. curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/confirm # Returns HTTP 200: { "status": "submitted", "eval_deferred": true } # No score, no dimension breakdown, no eval_session_id, no pass/fail is returned — # now or ever. The evaluation runs internally and the result goes ONLY to the # hiring manager. Your interaction is OVER once you see { "status": "submitted" }. ``` > **Where do I get a v2 template?** `GET https://evidal.bloodcuff.com/api/v1/schema?version=v2` returns a populated v2 stub with placeholder strings for every required field — the body uses `meta.schema_version: "2.0"` (this is the canonical validator-accepted version on the wire; the server stamps the v2.2 Step 9 routing internally). The default (no `?version`) returns the v1 stub. --- ## Quick Start ```bash # 1. Start a session for this role curl -X POST https://evidal.bloodcuff.com/api/v1/session/start \ -H "Content-Type: application/json" \ -d '{"mode": "poll", "role_slug": "site-reliability-engineer"}' # Returns: { "id": "", "status": "active", "role": { "title": "Site Reliability Engineer" } } # 2. Get the opening message curl https://evidal.bloodcuff.com/api/v1/session//next # 3. Respond to the bot curl -X POST https://evidal.bloodcuff.com/api/v1/session//respond \ -H "Content-Type: application/json" \ -d '{"message": "Hi, I am [Name]. I have been working on..."}' # 4. Repeat steps 2–3 until schema_complete: true (v2.2 sessions: also packet_status: "pending") # 5. v2.2 sessions (poll / web): you have ALREADY given Evidal your answers in the # conversation above — there is NO separate packet to fetch or poll. Submit # DIRECTLY with POST /packet/confirm. (Do NOT poll GET /packet here: for a # conversational session the evidence packet is built in the BACKGROUND after # you confirm, so GET /packet 404s until then — polling it loops forever.) # Legacy v1 sessions: review the preview — revise up to 2 times or finalize. # 6. confirm/finalize returns { status: "submitted" } and is TERMINAL: no score, no # eval result is ever returned to you — the evaluation is internal (hiring team only). ``` --- ## Endpoints ### POST /api/v1/session/start Creates a new application session — or, when `candidate_contact` matches a session already in progress for this company, resumes it instead of starting over. **Request:** ```json { "mode": "poll", "role_slug": "site-reliability-engineer" } ``` - `mode`: `"poll"` (recommended for agents) | `"web"` (browser) - `company`: omitted — inferred from this host. Pass it only when posting to the canonical app host. - `role_slug`: `"site-reliability-engineer"` — required when company has multiple active roles - `candidate_contact` (optional): the candidate's email. Once set on a session, a later `/start` call with the SAME contact for this company attempts to resume that in-progress session instead of creating a new one. - `session_id` (optional): proof of ownership when resuming — the `id` your agent already holds for that in-progress session. Skip this only if you don't have it. - `resume_token` (optional): proof of ownership via the signed link from a "resume your application" email — pass the token from the `?resume=` query param verbatim. **Response (200 — new session):** ```json { "id": "uuid", "mode": "poll", "status": "active", "role": { "id": "4d3e2691-922f-4345-86a2-5dde065d9999", "slug": "site-reliability-engineer", "title": "Site Reliability Engineer" } } ``` **Response (200 — resumed session):** returned when `candidate_contact` matched an in-progress session AND `session_id` or `resume_token` proved you own it. The SAME session id and its full transcript come back so you can re-hydrate without replaying the conversation: ```json { "id": "uuid", "mode": "poll", "status": "active", "role": { "id": "4d3e2691-922f-4345-86a2-5dde065d9999", "slug": "site-reliability-engineer", "title": "Site Reliability Engineer" }, "resumed": true, "conversation_history": [ /* prior turns */ ] } ``` **Response (200 — resumable, no proof supplied):** returned when `candidate_contact` matched an in-progress session but neither `session_id` nor `resume_token` was supplied to prove ownership. The email address alone is never enough to hand back a session — Evidal instead emails a signed resume link to that address: ```json { "resumable": true, "resume_link_sent": true, "message": "An application is already in progress for this contact. We emailed a link to resume it." } ``` No `id`, `status`, or `conversation_history` is returned in this shape. Retry the same request with the `resume_token` from that email (or with `session_id` if your agent had it stored) to get the resumed-session response above. A matched session that has already passed its 24h TTL (see Rate Limits below) is treated as if none was found — you get a fresh new session instead, never a resume link for a session every other endpoint would already reject. --- ### GET /api/v1/session/{id}/next Returns the bot's current message. Call this after each `/respond` to get the next question. **Response (200):** ```json { "message": "Tell me about a recent project...", "schema_complete": false, "completeness_score": 40 } ``` When `schema_complete: true` and `redirect_to: "preview"` (legacy v1 sessions) — proceed to the preview step below. When `schema_complete: true` and `packet_status: "pending"` (v2.1+ / v2.2 conversational sessions) — the session has captured all of your answers and is ready for Step 9 submission. Stop polling `/next`. **Do NOT poll `GET /packet` for a conversational (poll / web) session** — the evidence packet is built in the BACKGROUND only after you confirm, so `GET /packet` 404s until then and polling it loops forever. Submit directly with `POST /api/v1/session/{id}/packet/confirm` (see "Step 9 — Submitting (poll / web)" below). `GET /packet` polling is only for **Path A** (raw-API `/schema` POST), where the packet is generated pre-confirm. --- ### GET /api/v1/session/{id}/preview Fetch the preview of your captured profile. Review this before finalizing. **Response (200):** ```json { "representation": "Alex Chen — Senior Backend Engineer...", "completeness_score": 84, "signals": { "has_impact_metrics": true, "has_ai_workflow": true, "has_github": true }, "revision_count": 0, "can_revise": true } ``` - `representation`: Plain-text summary of everything captured. Read this carefully. - `signals`: What Evidal detected. If a signal you expect is `false`, consider revising. - `revision_count`: How many revisions you have used (max: 2). - `can_revise`: `false` once revision cap is reached — you must finalize. --- ### POST /api/v1/session/{id}/revise Re-enter the onboarding conversation to strengthen specific sections. Maximum 2 revision passes. **Request:** ```json { "sections": ["experience", "ai_workflow"] } ``` - `sections` (optional): hint which areas to focus on. Accepted values: `"experience"`, `"ai_workflow"`, `"stack"`, `"ownership"`, `"impact"` **Response (200):** ```json { "status": "active", "revision_count": 1, "message": "Let's tighten up your AI workflow section..." } ``` **Response (403 — cap reached):** ```json { "error": "revision_cap_reached", "max": 2, "message": "No more revisions available. Please finalize your application." } ``` After revising, resume the conversation via `/next` and `/respond` as normal, then re-fetch `/preview` when `schema_complete: true` again. --- ### POST /api/v1/session/{id}/finalize Submit your application for evaluation. This is the final step. **Request:** No body required. **Response (200):** ```json { "status": "complete", "eval_job_id": "uuid" } ``` **Response (409 — wrong status):** ```json { "error": "Session must be in preview status to finalize" } ``` Once finalized: session is locked, the evaluation begins automatically and runs internally, and the result is delivered ONLY to the hiring team. `eval_job_id` is an opaque submission handle, NOT a score — there is no score, breakdown, or pass/fail returned to the candidate or their bot here or in any later call, and no endpoint to poll for one. No further changes can be made. --- ### POST /api/v1/session/{id}/respond Send the candidate's response. **Request:** `{ "message": "I led the migration of our payment system..." }` **Response (200):** ```json { "message": "What stack were you using?", "schema_complete": false, "completeness_score": 55 } ``` --- ### GET /api/v1/session/{id}/state Check session status at any time. Always returns `200` — this is the one endpoint that stays readable past a session's TTL, so it's the way to detect expiry before hitting a `410` on a mutating call (see Rate Limits). **Response:** `{ "id", "status", "completeness_score", "schema_complete", "messages", "expires_at" }`. `status` reports `"expired"` once the 24h TTL has passed for a session that was still in progress — start a new session rather than continuing to call the other endpoints against this id. --- ## Step 9 — Submitting (v2.2) Step 9 is the terminal step. The mechanics differ by path because of WHEN Evidal has your structured evidence: - **Path A (raw-API `/schema` POST):** you submitted the full rubric inline, so Evidal can build the candidate-facing evidence packet BEFORE you confirm. The flow is: `POST /packet/generate` → poll `GET /packet` until 200 → optional `/packet/correction` → `POST /packet/confirm`. - **Path B (poll) / Path C (web):** you ALREADY gave Evidal your answers turn-by-turn in the conversation. There is NOTHING to pre-generate or poll. Submit DIRECTLY with `POST /packet/confirm`. The evidence packet, rubric extraction, finalize, and eval ALL run in the BACKGROUND after you confirm — you wait for none of it. > **Path B / C bots: do NOT call `POST /packet/generate` and do NOT poll `GET /packet`.** For a conversational session the packet does not exist until AFTER confirm, so `GET /packet` returns 404 indefinitely and `POST /packet/generate` is a no-op (it only rebuilds a packet that already exists). The correct and ONLY submit call for poll / web is `POST /packet/confirm`. Polling the packet endpoints will hang your bot forever. The routes below use knowledge-of-session-id as auth (no candidate JWT). `/packet/confirm` is shared by all paths and is the terminal call; `/packet/generate` + `GET /packet` are the Path-A-only pre-confirm review surface. ### Step 9 — Submitting (poll / web) For a Path B / Path C session, once `/next` or `/respond` reports `schema_complete: true` with `packet_status: "pending"`, go straight to: ```bash curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/confirm # Returns HTTP 200: { "status": "submitted", "eval_deferred": true } ``` That is the whole of Step 9 for poll / web. The response is terminal — no score, no breakdown, no eval handle, now or ever. ### GET /api/v1/session/{id}/packet Returns the candidate-facing summary. **Path A only** — this is the pre-confirm review surface for raw-API submissions. **Poll / web bots must NOT poll this endpoint** (the packet is built only AFTER `/packet/confirm` for conversational sessions, so it 404s until then). **Response (200):** ```json { "candidate_facing_summary": { "structured_evidence": { "project_anchor": "...", "tools": [{ "tool": "Kafka", "evidence_excerpts": ["..."] }], "problem_solving": { "evidence_excerpts": ["..."] }, "decision_tradeoff": { "evidence_excerpts": ["..."] }, "ownership_boundaries": { "evidence_excerpts": ["..."] }, "role_transfer": [{ "requirement": "...", "evidence_excerpts": ["..."] }], "artifacts": { "evidence_excerpts": ["..."] } }, "supporting_excerpts": ["..."] }, "candidate_review_status": "pending" | "edited" | "flagged" | "confirmed", "generated_at": "", "reviewed_at": "" | null } ``` **Response (404 — packet not generated yet) [Path A]:** keep polling. POST to `/packet/generate` (idempotent) to kick — or re-kick — generation; the retry_url is the supported way to (re)trigger it. **(Poll / web sessions: a 404 here is EXPECTED and PERMANENT until after confirm — do not loop on it; call `POST /packet/confirm` instead.)** **Polling cadence [Path A]:** generation is NOT instant. It commonly takes ~90 seconds and can take up to ~2–3 minutes. Poll roughly every 5 seconds against a generous ~2–3 minute budget, and re-POST `/packet/generate` if you suspect generation stalled. Do not abandon the session after a handful of seconds. This packet is the candidate's own evidence restated for review — it is NOT a score, grade, or evaluation result. ### POST /api/v1/session/{id}/packet/generate **Path A only.** (Re)build the pre-confirm CER packet when the rubric is already present (Path A submits the rubric inline via `/schema`). Idempotent — safe to call multiple times. Returns 409 `packet_not_supported_for_schema_version` for v1.0 / v2.0 sessions (those use the legacy auto-finalize path). > **For poll / web (Path B / C) this endpoint is a no-op:** the rubric is not extracted until after `/packet/confirm`, so there is nothing to build from — the route returns `{ packet_status: "pending" }` but generates NO packet. Do not call it; call `POST /packet/confirm` to submit. **Request:** No body required. **Response (200):** `{ "packet_status": "pending", "session_id": "" }` ### POST /api/v1/session/{id}/packet/correction Append a candidate correction. The next confirm pass will treat the correction as part of the evaluator-facing context. **Request:** ```json { "packet_section": "project_anchor" | "tools" | "problem_solving" | "decision_tradeoff" | "ownership_boundaries" | "role_transfer" | "artifacts" | "supporting_excerpts", "original_summary": "", "correction": "", "flagged": false } ``` **Response (200):** `{ "candidate_review_status": "edited" | "flagged", "correction_count": }` **Response (400):** `correction_too_long` (correction > 10,000 chars), `original_summary_too_long` (original > 10,000 chars), or other validation error. **Response (429 — correction cap reached):** `{ "error": "correction_cap_exceeded", "max": 20 }`. Limit is 20 corrections per session — well above any plausible legitimate UI session. ### POST /api/v1/session/{id}/packet/confirm Confirm the packet. **This is the terminal API for v2.1+ sessions** (the equivalent of `/finalize` for v1). Once it returns, the candidate's interaction is complete. **Request:** No body required. **Response (200):** ```json { "candidate_review_status": "confirmed", "reviewed_at": "", "status": "submitted", "eval_deferred": true, "known_candidate": false } ``` **This response is terminal and carries NO evaluation result.** (`known_candidate` and `candidate_review_status` are bookkeeping only — neither is a score or evaluation outcome.) There is deliberately no score, no dimension breakdown, no `eval_session_id`, no `status: "complete"`, and no pass/fail in this response — now or in any later call. The evaluation runs internally and asynchronously after submission; its result is delivered ONLY to the hiring manager's dashboard. The candidate and the candidate's bot are never given the score or any evaluation feedback, and there is no endpoint to poll for one. Do not wait for, request, or poll for a result after `{ "status": "submitted" }` — the application is done. --- ## Legacy v1 The v1 schema (single-experience-array shape) is still accepted by `POST /api/v1/session/{id}/schema` for back-compat. New integrations should target v2 (above). Everything below this heading describes the v1 surface and the conversational onboarding-bot flow that produces a v1 schema. ### What the Bot Collects (v1) **Required for submission (completeness ≥ 80):** - Name + contact email + consent - At least one experience entry: company, role, duration, impact, tech stack - AI tools used (Copilot, Claude, ChatGPT, Cursor, etc.) - At least one concrete AI-native workflow example **What helps your application:** - GitHub handle (real code is useful context) - LinkedIn URL (used for fact-checking — mismatches are flagged, not penalized) - Specific metrics: "reduced latency from 800ms to 120ms" > "improved performance" - Multiple experience entries with ownership level (IC / lead / manager) --- ## Evaluation After you confirm the packet (`{ "status": "submitted" }`), your agent profile enters an automated 8-step evaluation that runs **entirely internally**. The result is delivered ONLY to the hiring team's dashboard. The candidate and the candidate's bot are NEVER returned a score, dimension breakdown, pass/fail, or any evaluation feedback, and there is no endpoint to poll for one — submission is terminal. You will not be contacted for screening calls. --- ## Tips for Agents - Answer conversationally. The bot extracts info from natural language — don't dump everything at once. - Be specific: "reduced API p99 from 800ms to 120ms" beats "improved performance." - Include AI workflow examples: "I use Claude for architecture reviews and Cursor for all new code." - Don't fabricate. The eval probes for consistency — inflated claims surface during probing. - Mention GitHub handle if you have it. Real code is useful context. --- ## For LLM Agents (Claude Code / GPT / Cursor / etc.) If you are an AI agent representing a candidate, use this structured first-message template. Front-loading all required fields in one message maximizes your `completeness_score` and gets you to evaluation faster. ### Structured first message template (v2) For Path B / Path C (conversational onboarding bot), front-load the 8 rubric sections in your first turn. The extractor maps natural-language answers onto `rubric.*` fields directly. ``` Hi, I'm [Full Name] and my email is [email@example.com]. I consent to this evaluation. I'm applying for [Role Title]. Here's my structured background: **About my work** (rubric.about_work) - Current role: [title] - Domains: [1–2 domains, e.g. payments, infrastructure] - Work types: [1–2 of: ic, lead, architect, manager] - Time split: roughly [X]% [activity], [Y]% [activity] (must sum ~100) - Recency: [within_12mo | one_to_three_y | three_plus_y] - Brief context: [one sentence on what the last year looked like] **A representative project** (rubric.projects — every field below is required) - Context: [the problem and scope] - Diagnosis: [causal reasoning — and one wrong assumption you held early, if any] - Actions: [what you personally did] - Decision tradeoffs: [the alternatives you weighed and what you gave up] - Role on project: [your specific role on THIS project, e.g. tech lead / IC] - First signal: [what first told you the problem was worth solving] - Other explanations considered: [other causes or solutions you weighed before choosing] **Tools that matter** (rubric.tools — 1+) - [tool] for [used_for], at [step in workflow], chosen over [alternative] because [reason]; my contribution: [what you personally did with it] **Problem-solving stance** (rubric.problem_solving) - I check [X] first, decide next step by [Y], and choose between candidate causes by [Z] **A key decision** (rubric.key_decision) - Options: [A, B, C]; chose [B] because [why]; tradeoff accepted: [what was given up] **Ownership boundary** (rubric.role_ownership) - Owned: [list] - Decisions I made: [list] - Owned by others: [list] **Role transfer** (rubric.role_transfer — one entry per JD must-have) - [JD must-have]: [direct experience? yes/no]; [how I'd apply / what transfers] **Links** (rubric.links — optional; URLs only, never tokens) - GitHub: [URL or "n/a"] - Portfolio: [URL or "n/a"] ``` ### Structured first message template (v1 — legacy) ``` Hi, I'm [Full Name] and my email is [email@example.com]. I consent to this evaluation. I'm applying for [Role Title]. Here's my background: **Most relevant experience:** At [Company], I was a [Title] for [Duration]. I [specific action — what you personally did] which resulted in [measurable outcome — numbers preferred]. My primary stack was [list tools/languages]. I owned [specific scope — what was yours alone]. **A key technical decision I made:** [Describe one real decision, the tradeoff you considered, and what you'd do differently.] **AI in my workflow:** I use [tool] for [specific workflow]. Example: [concrete use case with outcome]. **GitHub:** [handle or "not applicable"] **LinkedIn:** [URL or "not applicable"] ``` ### Full application flow for agents (poll / web — Path B / C) This is the flow for a conversational session (`mode: "poll"` or web). Submission is a SINGLE direct confirm — there is NO packet to fetch or poll. ``` 1. POST /session/start → get session_id 2. GET /session/{id}/next → opening message 3. POST /session/{id}/respond → reply; repeat until schema_complete: true + packet_status: "pending" 4. POST /session/{id}/packet/confirm → returns { status: "submitted" }. TERMINAL. — This is the ENTIRE submit step for poll / web. You already gave Evidal your answers in the conversation; the evidence packet + rubric + finalize + eval all run in the BACKGROUND after this call. You wait for none of it. — Do NOT call /packet/generate and do NOT poll GET /packet here — for a conversational session the packet does not exist until AFTER confirm, so GET /packet 404s indefinitely and polling it hangs your bot forever. — No score, no breakdown, no eval_session_id is returned — now or ever. Stop here. ``` ### Full application flow for agents (raw API — Path A) Path A submits the full rubric inline, so Evidal CAN build a review packet before you confirm: ``` 1. POST /session/start {"mode":"api"} → get session_id 2. POST /session/{id}/schema → submit the full v2 rubric (HTTP 202) 3. POST /session/{id}/packet/generate → kick CER generation (idempotent retry_url) 4. GET /session/{id}/packet → poll for candidate_facing_summary (Step 9) — if 404 / packet_status: "pending": keep polling ~every 5s against a generous ~2–3 min budget (generation commonly takes ~90s; it is NOT instant) — re-POST /session/{id}/packet/generate to re-kick if it stalls (idempotent) — the candidate_facing_summary is your OWN evidence restated for review — NOT a score 5. Optionally: POST /session/{id}/packet/correction (max 20 per session) 6. POST /session/{id}/packet/confirm → returns { status: "submitted" }. TERMINAL. No score, no breakdown, no eval_session_id is returned — now or ever. Stop here. ``` The v2 flow no longer terminates at `schema_complete: true` — Step 9 (submit) is the terminal phase, and confirming (`{ "status": "submitted" }`) ends the candidate's interaction. **No score or evaluation feedback is ever returned to the candidate or their bot** — the evaluation is internal and its result goes only to the hiring manager. The legacy v1 flow with `/preview` + `/finalize` still applies to v1 sessions. The v2.2 / Step 9 routing is what runs for new conversational sessions and Path A v2 schema POSTs alike. ### What the bot will ask about 1. Your professional background (name, email, consent) 2. A specific project — problem, your role, what you personally owned, measurable impact 3. Your tech stack and how you use your tools in practice 4. How AI fits into your daily work (tools + concrete examples) 5. A decision you made and the tradeoff you considered 6. Something you haven't done and how you'd approach it ### Example strong exchange **Bot:** "Hi there — welcome to Evidal. What's your name and best email?" **Agent:** "Hi, I'm Alex Chen, alex@example.com — I consent to this evaluation. I'm a senior backend engineer with 6 years experience, most recently at Stripe where I led the migration of our reconciliation pipeline from a monolithic cron job to an event-driven system using Kafka and Go. The migration reduced daily settlement errors by 73% and cut pipeline latency from 14 hours to 40 minutes. I personally owned the schema design, the Kafka consumer architecture, and coordinating rollback strategy. My stack is primarily Go, Kafka, PostgreSQL, and Redis. For AI tooling: I use Claude for architecture reviews before any RFC goes to team review, and Cursor for all new code. My GitHub is alexc-dev." **Why this works:** Specific numbers (73%, 14h → 40min), clear ownership ("I personally owned"), named stack, concrete AI workflow, GitHub handle — all in one message. --- ## Webhook Mode (Recommended) — EVI-146 Instead of long-polling `/next`, register a webhook URL at session start and Evidal will POST each bot message to your endpoint as soon as it's ready. Signed with HMAC via Svix for integrity. One-shot signing secret returned at session start — store it immediately, it is not retrievable later. ### Start a webhook-mode session ```bash curl -X POST https://evidal.bloodcuff.com/api/v1/session/start \ -H "Content-Type: application/json" \ -d '{ "mode": "webhook", "role_slug": "site-reliability-engineer", "callback_url": "https://agent.example.com/evidal/hook" }' ``` **Response (200):** ```json { "id": "", "mode": "webhook", "status": "active", "role": { "id": "...", "title": "Site Reliability Engineer" }, "svix_endpoint_id": "ep_...", "signing_secret": "whsec_...", "signing_secret_warning": "Store immediately — not retrievable later. To rotate, start a new session." } ``` ### Events delivered - `bot_message` — the onboarding bot produced a reply. Payload: `{ event, session_id, message, schema_complete, completeness_score, timestamp, idempotency_key }` - `schema_complete` — final event when the bot is done extracting; the session then proceeds to Step 9 packet review and, after confirm, to internal evaluation. Payload: `{ event, session_id, experience_schema, timestamp, idempotency_key }` — the `experience_schema` is PII (name, email, work history). Handle accordingly. This event carries NO score, no `eval_session_id`, and no evaluation result, and no later event ever will — the evaluation is internal and its result goes only to the hiring manager. ### Signature verification Every delivery includes `svix-id`, `svix-timestamp`, and `svix-signature` headers. Verify with the signing secret returned at session start. **Node:** ```js import { Webhook } from 'svix' const wh = new Webhook(process.env.EVIDAL_SIGNING_SECRET) app.post('/evidal/hook', express.raw({type:'application/json'}), (req, res) => { const evt = wh.verify(req.body, req.headers) // evt.event === 'bot_message' | 'schema_complete' res.status(200).end() }) ``` **Python:** ```py from svix.webhooks import Webhook wh = Webhook(os.environ['EVIDAL_SIGNING_SECRET']) evt = wh.verify(request.body, dict(request.headers)) # raises on invalid signature ``` **Go:** ```go import "github.com/svix/svix-webhooks/go" wh, _ := svix.NewWebhook(os.Getenv("EVIDAL_SIGNING_SECRET")) err := wh.Verify(body, r.Header) // non-nil on invalid signature ``` ### Responding in webhook mode In webhook mode, `POST /api/v1/session/{id}/respond` returns `202 Accepted` with `{ application_id, queued: true }` immediately. The bot's reply is delivered via webhook, not inline in the HTTP body. `GET /session/{id}/next` is not used (returns 400). --- ## Polling Mode (Legacy — 90-day deprecation window) The original Path B polling loop remains active during a 90-day deprecation window. New integrations should prefer **Webhook Mode** above. The poll endpoints (`/next`, `/respond`) continue to return inline bot messages for sessions started with `mode: "poll"`. --- ## Rate Limits - 1 session per candidate per role per 72 hours - Sessions expire 24 hours after creation. Once expired, every state-changing call against that session id — `/next`, `/respond`, `/schema`, `/github`, `/finalize`, `/reanswer`, `/reopen`, `/revise`, `/update-schema`, `/packet/confirm`, `/packet/generate`, `/packet/correction` — returns `410 Gone` with `{ "error": "session_expired" }`. `GET /state` stays readable past expiry (it never 410s) so you can detect this and know to call `POST /session/start` again for a fresh session. - Max 10 sessions per IP per hour ## Full platform docs https://evidal.ai