How it works
The Resolve job lifecycle from triage to attestation, what the public proof document actually proves, and what the sandboxed runner will and will not touch.
A Resolve job is a small state machine with one hard rule at the end: the charge cannot exist without the attestation. Everything below is how a job gets there — or doesn't.
Job lifecycle
POST /v1/resolve/jobs
│
▼
triage ──── decline ───▶ declined ($0, final)
│
quote t7|t19|t49
│
├── free tier left ──▶ accepted (auto)
│
▼
quoted ── POST …/accept ──▶ accepted
│
▼
running ──▶ resolved (attestation + charge)
│
├──────▶ failed ($0 — no verified fix within budget)
└──────▶ error ($0 — infrastructure; safe to resubmit)| Status | Meaning | Billed? |
|---|---|---|
triage | Transient — the issue is being read. You never see this in a response. | — |
declined | Triage judged a verified fix unlikely (wrong stack, no clear repro, needs credentials…). decline_reason says why. | No |
quoted | A flat price was set (tier, quote_micro_usd). Nothing runs until you accept. | Not yet |
accepted | You accepted (or the free tier applied). Queued for a runner. | Not yet |
running | The sandbox is cloning, reproducing, fixing, verifying. | Not yet |
resolved | Repro flipped fail → pass, regression suite green, attestation written. attestation_url is set. | Yes — the quote |
failed | No candidate passed both gates within the turn and time budget. fail_reason explains. | No |
error | Environment or infrastructure problem (broken clone, sandbox unavailable, worker lost). Resubmit freely. | No |
Statuses only move forward; resolved, failed, error and declined are terminal. GET /v1/resolve/jobs/{id} is scoped to the key's user — another user's job id is a 404.
Triage
Runs inline in your POST, against the codai model, and answers with exactly one line: QUOTE t7|t19|t49 <reason> or DECLINE <reason>. Rough meaning of the tiers: t7 — small, well-scoped (single file, clear repro); t19 — moderate (multi-file, needs investigation); t49 — hard but plausible (subtle, cross-cutting). If triage itself is unavailable the job is declined with triage unavailable, please retry — Resolve never quotes blind.
The free tier
Each user gets 3 free verified fixes. The claim is atomic: when a quotable job comes in and you still have a free slot, the job is created as tier: "free", quote_micro_usd: 0, status: "accepted" and starts immediately. free_tier_remaining in the response tells you how many are left after this one. A free job that ends failed or error gives the slot back.
Accept
Only quoted jobs accept; anything else is 409 job is <status>, only quoted jobs can be accepted. Accepting is the moment you agree to the quote — but you still pay only if it resolves.
Running
Reproduce first
The runner clones the repo at HEAD (recorded as base_commit). If you supplied test_command, it runs that: it must fail on the base commit, otherwise the job ends with supplied test_command already passes on HEAD — nothing to fix. Without a command, the agent writes a repro test — a single pytest / vitest / jest file — and confirms it fails. That failing output becomes test_output_before.
Fix
The agent gets the issue, the repo and the failing test, and edits code through a bounded loop of at most 4 turns per candidate (SEARCH/REPLACE edits plus file reads). Paid tiers fan out up to 4 candidates (the first at temperature 0, the rest at 0.8) and stop at the first one that passes; the free tier runs one. A candidate passes only if the repro now passes and the repository's own suite still passes.
Verify
Verification never fails open. Two commands run in the sandbox on base_commit + patch: the FAIL_TO_PASS command (test_command) and a PASS_TO_PASS regression command (regression_command) — the repo's full suite with the repro file excluded. Environment breakage, timeouts or a runner that vanished are error, never resolved.
Attest, then charge
The attestation row is written, then — and only then — the resolve_charges row for the quoted amount. If anything between the two fails, you have a proof and no bill, never the reverse.
What the attestation proves
GET /v1/resolve/attestations/{id} needs no authentication: the UUID is the capability. Anyone with the link can audit the fix. It returns:
{
"id": "a45764d8-…",
"kind": "codai.resolve.attestation.v0",
"repo_url": "https://github.com/owner/repo",
"base_commit": "3f9c…",
"patch": "diff --git a/src/parse.ts b/src/parse.ts\n…",
"repro_test": { "path": "tests/test_issue_42.py", "content": "…" },
"test_command": "python -m pytest -q tests/test_issue_42.py",
"regression_command": "python -m pytest -q --deselect tests/test_issue_42.py --ignore=tests/test_issue_42.py",
"test_output_before": "… 1 failed …",
"test_output_after": "… 1 passed …",
"regression_output": "… 212 passed …",
"runner_image_digest": "sha256:…",
"started_at": "2026-09-23T09:12:04.118Z",
"verified_at": "2026-09-23T09:19:41.402Z"
}| Field | What it lets you check |
|---|---|
repo_url + base_commit | The exact tree the fix was verified against. git checkout <base_commit> and reproduce it yourself. |
patch | The complete unified diff. Nothing outside it was changed. |
repro_test | The test that encodes the bug (null when you supplied test_command). Read it to judge whether it captures the issue. |
test_command / test_output_before | The FAIL_TO_PASS command and its output on the base commit — the "before". |
test_output_after | The same command after the patch — the "after". |
regression_command / regression_output | The repo's own suite, still green. |
runner_image_digest | The container image the tests ran in, pinned by digest. |
started_at / verified_at | Wall-clock window of the attempt. |
What it does not prove: that the repro test is the right test for the issue, or that the fix is the one you would have written. That is what the diff and the test content are there for — read them like a PR, with the advantage that the execution evidence is attached.
The attestation is immutable and stays public; the same URL is what the GitHub Action links from the PR.
Signed attestations
Since 2026-09-27 every new attestation also carries signature: a DSSE v1 envelope signed with an ECDSA P-256 / SHA-256 key held in Cloud KMS. You do not have to trust the JSON we serve — check it against the signature.
What is signed is a canonical statement (RFC 8785 JSON, payloadType application/vnd.codai.resolve.attestation+json;v=1) that binds every field above, bodies by their sha256 (lowercase hex of the UTF-8 bytes):
{
"kind": "codai.resolve.attestation.v1",
"attestation_id": "a45764d8-…",
"repo_url": "https://github.com/owner/repo",
"base_commit": "3f9c…",
"patch_sha256": "…",
"repro_test": { "path": "tests/test_issue_42.py", "sha256": "…" },
"test_command": "python -m pytest -q tests/test_issue_42.py",
"regression_command": "python -m pytest -q …",
"test_output_before_sha256": "…",
"test_output_after_sha256": "…",
"regression_output_sha256": "…",
"runner_image_digest": "sha256:…",
"started_at": "2026-09-23T09:12:04.118Z",
"verified_at": "2026-09-23T09:19:41.402Z"
}repro_test is null when you supplied test_command; a missing regression run gives null for both regression fields. The envelope is { payloadType, payload: base64(statement), signatures: [{ keyid, sig }] }, and sig is a DER signature (base64) over the DSSE PAE: "DSSEv1 " + len(payloadType) + " " + payloadType + " " + len(payload) + " " + payload (byte lengths).
Where the key is: GET /v1/resolve/keys (public, CORS-open, cached 1 h) returns { keys: [{ keyid, algorithm: "ECDSA_P256_SHA256", payload_type, encoding, public_key_pem }] }. Pin the PEM if you want verification that does not depend on our servers at all.
A valid attestation passes both checks: the signature verifies with a published key, and the decoded statement equals the statement recomputed from the attestation's own fields. A valid signature over different content is a failure (STATEMENT_MISMATCH).
How to verify:
codaid resolve verify a45764d8-… # or the full attestation URL; no API key needed
# VERIFIED (key projects/…/cryptoKeyVersions/1) — exit 0
# NOT VERIFIED: STATEMENT_MISMATCH — exit 1const proof = await codai.resolve.attestation(job.attestation_url!);
const r = await codai.resolve.verifyAttestation(proof); // { keys } to pin
// { ok: true, keyid } | { ok: false, reason: 'UNSIGNED' | 'BAD_SIGNATURE' | 'STATEMENT_MISMATCH' | 'BAD_PAYLOAD_TYPE' }proof = client.resolve.attestation(job["attestation_url"])
r = client.resolve.verify_attestation(proof) # needs `pip install cryptography` for the signature stepAttestations created before 2026-09-27 are unsigned (signature: null) and verify as UNSIGNED; they are still valid execution evidence, just not cryptographically bound.
Webhooks
Instead of polling GET /v1/resolve/jobs/{id}, pass callback_url on intake — an https URL on a public host (no credentials in the URL, no localhost / private / link-local / metadata addresses, at most 2000 characters). The 201 quote or 200 decline response then carries callback_secret (whsec_…). It is returned once and never shown again — store it with the job id.
{ "repo_url": "https://github.com/owner/repo", "issue_url": "https://github.com/owner/repo/issues/42",
"callback_url": "https://ci.example.com/hooks/codai-resolve" }Events
| Event | Sent when |
|---|---|
job.quoted | A paid quote is ready — call /accept. |
job.declined | Triage declined; decline_reason is set. |
job.resolved | Verified fix; attestation_id and an absolute attestation_url are set. |
job.failed | No verified fix within budget; the wallet hold is refunded. |
job.error | Infrastructure failure; the hold is refunded, resubmit freely. |
Each delivery is a POST with content-type: application/json and these headers:
| Header | Value |
|---|---|
x-codai-event | The event name, same as event in the body. |
x-codai-delivery | <jobId>:<event> — identical on every retry; use it to deduplicate. |
x-codai-signature | t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(callback_secret, "<t>.<raw body>"). |
{
"event": "job.resolved",
"job": {
"id": "6f1d2c3e-…", "status": "resolved", "tier": "t19", "quote_micro_usd": 19000000,
"repo_url": "https://github.com/owner/repo", "decline_reason": null, "fail_reason": null,
"cost_micro_usd": 1830000, "attestation_id": "a45764d8-…",
"attestation_url": "https://resolve.codai.ro/v1/resolve/attestations/a45764d8-…",
"completed_at": "2026-09-27T09:19:41.402Z"
},
"sent_at": "2026-09-27T09:19:42.010Z"
}Verify the signature
Verify over the raw request body, before parsing JSON; the SDK helpers compare in constant time and reject a timestamp more than 300 seconds away from now.
import { verifyResolveWebhook, type ResolveWebhookEvent } from 'codai-sdk';
export async function POST(req: Request) {
const raw = await req.text();
if (!verifyResolveWebhook(process.env.RESOLVE_WHSEC!, raw, req.headers.get('x-codai-signature'))) {
return new Response('bad signature', { status: 401 });
}
const evt = JSON.parse(raw) as ResolveWebhookEvent;
if (evt.event === 'job.resolved') console.log(evt.job.attestation_url);
return new Response(null, { status: 204 });
}import json, os
from codai import verify_resolve_webhook
@app.post("/hooks/codai-resolve") # FastAPI
async def resolve_hook(request: Request):
raw = await request.body()
if not verify_resolve_webhook(os.environ["RESOLVE_WHSEC"], raw, request.headers.get("x-codai-signature")):
raise HTTPException(401, "bad signature")
evt = json.loads(raw)
return {"ok": True}Delivery semantics
- Up to 3 attempts per event (backoff 1 s, then 4 s) on a
5xx, a429or a network error; any other non-2xx stops retrying. Each attempt times out after 10 s. Answer2xxquickly and do the work asynchronously. - Redirects are not followed, and every DNS answer for the host is re-checked to be public at delivery time.
- Delivery never changes job state or billing — a dead endpoint does not fail a job.
GET /v1/resolve/jobs/{id}stays the source of truth; poll it if you missed an event.
Safety: what the runner touches
Resolve runs untrusted repository code — your tests, your install scripts — so the execution model is deliberately narrow.
- Sandboxed execution. Tests, setup and the fix candidates run in a separate, powerless Cloud Run job (
codai-swe-exec) with no database access and no gateway credentials; results come back as artifacts. The API process itself only clones the repo, reads files and builds diffs. In production the API refuses to run repository code in-process — a misconfiguration turns intoerror, never into code running beside secrets. - Public repos, read-only. Resolve fetches
https://github.com/<owner>/<repo>anonymously. It never pushes; the GitHub Action does that with your token in your workflow. - Diff-only changes. The patch in the attestation is the full extent of what changed. The agent edits via SEARCH/REPLACE blocks; it cannot run arbitrary shell in the API process.
- Bounded budgets. At most 4 agent turns per candidate, at most 4 candidates, a per-job timeout of 30 minutes with a sandbox run limit of 15 minutes each, and an orphan watchdog that marks a job
error(and returns a free slot) if its worker disappears. - Nothing hidden. The model calls happen on the
codaigateway under a session idresolve-<jobId>-…, so the upstream cost per job is auditable inusage_eventslike any other request.
Private beta means an honest limits list: public github.com repos only; Python (pytest) and JavaScript/TypeScript (vitest, jest, node:test); repos whose tests need secrets, services or non-trivial system setup will usually be declined or end in error.
Last updated:
GitHub Action
Install the Codai Resolve action — label an issue codai-fix and get a PR with a verified fix and a public attestation. Inputs, outputs, secrets, permissions and example workflows.
Pricing and limits
Flat quotes of €7, €19 or €49 held on your euro wallet and kept only on a verified fix; three free fixes; top-ups on 402; per-user caps and timeouts.