{"openapi":"3.1.0","info":{"title":"zkVerifier","version":"0.1.0"},"paths":{"/health":{"get":{"summary":"Health","description":"Always-healthy probe. No DB, Redis, or HTTP calls.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Health Get"}}}}}}},"/ready":{"get":{"summary":"Ready","description":"Return 200 if all three components healthy, 503 otherwise.\n\nChecks postgres (A.3.3a), oracle freshness (A.3.3b), and worker\nreachability (A.3.3b).  Returns a JSON status dict with each component.\n\nFor oracle: if the DB has a fresh tick OR an external price source\n(Coinbase/CoinGecko/Alchemy) is available, the oracle is healthy.","operationId":"ready_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/proofs/{job_id}":{"get":{"tags":["proofs"],"summary":"Get Job","description":"Return the job state for the given job ID (AC1 of SPRINT-5).\n\nReturns 200 with the full job record including quote and links.\nReturns 404 when no job matches the ID.","operationId":"get_job_v1_proofs__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/proofs":{"post":{"tags":["proofs"],"summary":"Receive Proof","description":"Handle POST /v1/proofs — validate, hash, idempotency check, return response.\n\nValidation order (each returns a specific error code):\n1. Body size > 2 MiB → 413 PAYLOAD_TOO_LARGE\n2. Body parse fails (empty/invalid) → 422 PROOF_INVALID\n3. proofType not in (ultrahonk, groth16) → 422 PROOF_TYPE_UNSUPPORTED\n4. version not allowed → 422 PROOF_VERSION_UNSUPPORTED\n5. Proof format invalid → 422 PROOF_INVALID\n6. Idempotency: same key + same hash → 200 (existing job)\n7. Idempotency: same key + different hash → 409 (conflict, A.4.5)\n8. No existing key → insert new job → 201 (or 200 on proof_hash collision)","operationId":"receive_proof_v1_proofs_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/vk":{"post":{"tags":["vk"],"summary":"Register Vk","description":"Handle POST /v1/vk — register a verification key.\n\nReturns 200 {vk_hash, registered_at} on first registration.\nReturns 409 {code: \"VK_ALREADY_REGISTERED\", existingHandle} on duplicate.","operationId":"register_vk_v1_vk_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_VkRegisterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/vk/{vk_hash}":{"get":{"tags":["vk"],"summary":"Get Vk","description":"Handle GET /v1/vk/{vk_hash} — look up a registered VK.\n\nReturns 200 {vk_hash, registered_at, usage_count} on success.\nReturns 404 {code: \"VK_NOT_FOUND\"} if the handle does not exist.","operationId":"get_vk_v1_vk__vk_hash__get","parameters":[{"name":"vk_hash","in":"path","required":true,"schema":{"type":"string","title":"Vk Hash"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/internal/vk/register":{"post":{"tags":["vk"],"summary":"Store Chain Statement Hash","description":"Handle POST /v1/internal/vk/register — worker stores chain statementHash.\n\nCalled by the worker after successfully registering a VK on-chain.\nUpdates the vk_registrations row with the chain's statement_hash.\nIdempotent: if statement_hash is already set, returns 200 (no-op).","operationId":"store_chain_statement_hash_v1_internal_vk_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_WorkerVkRegisterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/proofs/{job_id}/fulfill":{"post":{"tags":["fulfill"],"summary":"Fulfill","description":"Handle POST /v1/proofs/{id}/fulfill.\n\nValidation order:\n1. Look up the job (needed for both payment and no-header paths).\n2. PAYMENT-SIGNATURE (canonical, x402 v2) or X-PAYMENT (alias) header\n   present → verify, validate, settle, transition.\n3. No payment header → check quote status, return 402 with\n   PaymentRequired (which includes the PAYMENT-REQUIRED header).","operationId":"fulfill_v1_proofs__job_id__fulfill_post","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}},{"name":"Payment-Signature","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment-Signature"}},{"name":"X-Payment","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Payment"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/proofs/{job_id}/events":{"get":{"tags":["events"],"summary":"Job Events","description":"Return a JSON snapshot of the event log for the given job (Path A).\n\nReturns 200 with a JSON array of all events for the job:\n``[{ts, type, ...}, ...]``. Clients poll this endpoint.\n\nReturns 404 with a clean error body when no events exist for the job.\n\nDesign decision (Path A): for the early-stage API we return a\nJSON array snapshot rather than a true SSE stream.  The SSE\nendpoint (``StreamingResponse``) was previously at this path\nand is kept for backwards compatibility — FastAPI routes the\n``Response`` (JSON) here by default.  If a true SSE consumer\nis needed later, it can be placed at ``/v1/sse/{job_id}``.","operationId":"job_events_v1_proofs__job_id__events_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sse/{job_id}":{"get":{"tags":["events"],"summary":"Job Events Sse","description":"Return an SSE stream of lifecycle events for the given job.\n\nThis endpoint lives at ``/v1/sse/{job_id}`` rather than the\nsnapshot path so that JSON consumers and SSE consumers do not\nconflict (FastAPI can only return one response type per route).","operationId":"job_events_sse_v1_sse__job_id__get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/lookup/{address}":{"get":{"tags":["lookup"],"summary":"Lookup Address","description":"Handle GET /v1/lookup/{address}.\n\nReturns recent jobs where the payer_address (payments.from_address)\nmatches the given Ethereum address.\n\nValidation order:\n1. Address must be 0x[40 hex chars] → 422\n2. Network must be base-sepolia | base → 422\n3. Limit clamped to [1, 100] (FastAPI Query constraint)\n\nResult is cached in Redis under key lookup:{address}:{network} with\nTTL from LOOKUP_CACHE_TTL_SECONDS.","operationId":"lookup_address_v1_lookup__address__get","parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","title":"Address"}},{"name":"network","in":"query","required":false,"schema":{"type":"string","default":"base-sepolia","title":"Network"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/jobs/by-status":{"get":{"summary":"Jobs By Status","description":"Return jobs in the given status for worker boot recovery (B.3.5).\n\nThe worker calls this on startup to find jobs that were in 'paid' or\n'submitting' state when it crashed. It then resumes their submission\nflow.\n\nParameters:\n    status: 'paid' or 'submitting'.\n    engine: database engine from FastAPI Depends.\n\nReturns:\n    List of job dicts (with id, status, proof_payload, txHash).","operationId":"jobs_by_status_internal_jobs_by_status_get","parameters":[{"name":"status","in":"query","required":true,"schema":{"type":"string","description":"Job status to filter by (paid or submitting)","title":"Status"},"description":"Job status to filter by (paid or submitting)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobsByStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/job/fail":{"post":{"summary":"Job Fail","description":"Mark a job as failed with a reason.\n\nCalled by the worker when it detects a definitive submit failure\n(e.g. 1010 RPC error). Prevents boot recovery from re-queueing\nthe same failing job.\n\nReturns {ok: \"true\"} on success, or 400/500 on error.","operationId":"job_fail_internal_job_fail_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobFailRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Job Fail Internal Job Fail Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/internal/submit":{"post":{"summary":"Submit Mock","description":"Mock worker submit endpoint.\n\nIn production, the Node worker processes this request and emits events\nto the SSE stream. Here, we populate the in-memory event store directly\nso the smoke test can verify the full event sequence.\n\nReturns {listenHandle, jobId} — matching the real worker's response.","operationId":"submit_mock_internal_submit_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Submit Mock Internal Submit Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/internal/chain-info":{"post":{"summary":"Update Chain Info","description":"Handle POST /v1/internal/chain-info.\n\nCalled by the Node worker when zkVerify emits includedInBlock or\nfinalized events. Persists zk_tx and zk_block_hash to the jobs table.\n\nReturns 200 with the updated values. Returns 404 if the job doesn't exist.","operationId":"update_chain_info_v1_internal_chain_info_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChainInfoRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Update Chain Info V1 Internal Chain Info Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/pay":{"get":{"tags":["deprecated"],"summary":"Deprecated Pay","description":"Return 410 Gone for the old /v1/pay endpoint.\n\nReplaced by POST /v1/proofs/{id}/fulfill (x402 payment flow).","operationId":"deprecated_pay_v1_pay_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["deprecated"],"summary":"Deprecated Pay","description":"Return 410 Gone for the old /v1/pay endpoint.\n\nReplaced by POST /v1/proofs/{id}/fulfill (x402 payment flow).","operationId":"deprecated_pay_v1_pay_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/price":{"get":{"tags":["price"],"summary":"Get Price","description":"Handle GET /v1/price.\n\nReads the latest oracle_ticks row and returns the VFY/USDC price\nin atomic units (integer).  Returns 503 if no tick exists or the\nlatest tick is older than 5 minutes.\n\nPer Mr. V 2026-09-24: the request path NEVER calls external APIs.\nStale or empty data = refuse with 503 (fail-closed).\n\nResponse shape (200):\n    {\n        \"pair\": \"VFY/USDC\",\n        \"price\": 140123456789012345678,\n        \"atomic_unit\": \"USDC\",\n        \"updated_at\": \"2026-09-22T12:00:00+00:00\",\n        \"source\": \"uniswap_v4\"\n    }\n\nResponse shape (503):\n    {\n        \"error\": \"oracle data stale, retry shortly\",\n        \"stale_threshold_seconds\": 300\n    }","operationId":"get_price_v1_price_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/docs":{"get":{"tags":["docs"],"summary":"Docs Page","description":"Serve the usage documentation page.","operationId":"docs_page_docs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"ChainInfoRequest":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"UUID of the job whose chain info to update"},"zk_tx":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zk Tx","description":"zkVerify transaction hash"},"zk_block_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zk Block Hash","description":"zkVerify block hash from block explorer"}},"type":"object","required":["job_id"],"title":"ChainInfoRequest","description":"Body for POST /v1/internal/chain-info (worker contract).\n\nFields:\n    job_id: UUID of the job to update\n    zk_tx: zkVerify transaction hash (nullable — may be set by includedInBlock event)\n    zk_block_hash: zkVerify block hash (nullable — may be set by finalized event)"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobFailRequest":{"properties":{"job_id":{"type":"string","title":"Job Id"},"failure_reason":{"type":"string","title":"Failure Reason"}},"type":"object","required":["job_id","failure_reason"],"title":"JobFailRequest","description":"Body for POST /v1/internal/job/fail.\n\nThe worker calls this when it detects a definitive submit failure\n(e.g. 1010 'Inability to pay some fees' in the inline-VK path).\nSets job status to 'failed_submit' and persists the failure_reason\nso that boot recovery (recoverPaidAndSubmittingJobs) does not\nre-queue the job."},"JobsByStatusResponse":{"properties":{"jobs":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Jobs"}},"type":"object","required":["jobs"],"title":"JobsByStatusResponse","description":"Body for GET /internal/jobs/by-status (worker recovery contract)."},"SubmitRequest":{"properties":{"job_id":{"type":"string","title":"Job Id"},"proof_payload":{"additionalProperties":true,"type":"object","title":"Proof Payload"}},"type":"object","required":["job_id","proof_payload"],"title":"SubmitRequest","description":"Body for POST /v1/internal/submit (mock worker contract).\n\nFields mirror the real Node worker contract (worker_client.py)."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"_VkRegisterRequest":{"properties":{"vk":{"type":"string","title":"Vk","description":"Verification key as a hex string (0x-prefixed or not)."}},"type":"object","required":["vk"],"title":"_VkRegisterRequest","description":"Parsed POST /v1/vk request body.\n\nFields:\n    vk: hex string representing the verification key (with or without 0x prefix)."},"_WorkerVkRegisterRequest":{"properties":{"vk_hash":{"type":"string","title":"Vk Hash","description":"Our sha256 vk_hash for this VK."},"chain_statement_hash":{"type":"string","title":"Chain Statement Hash","description":"Chain statementHash from registerVerificationKey event (H256, 0x-prefixed)."}},"type":"object","required":["vk_hash","chain_statement_hash"],"title":"_WorkerVkRegisterRequest","description":"Parsed POST /v1/internal/vk/register request body."}}}}