TensorCash.Verify

Reference

API reference

Base URL https://api.verify.tensorcash.boblabs.eu. All replies are JSON. The machine-readable version is openapi.yaml.

Conventions

Authentication. Every endpoint except GET /healthz takes a key from the portal: Authorization: Bearer tvk_…. Portal keys carry verify:invoke and verify:full; verify:share-mode is added on request for share verification. Missing or unknown key: 401. Missing scope: 403.

Network. Mainnet only. /v1/… and /net/tensor-main/v1/… are the same routes.

Submit bodies. The raw bytes of a ValidationRequest FlatBuffer whose request union is a BlockValidation (schema shared-utils/fb-schemas/validation.fbs in the TensorCash repository), sent as Content-Type: application/octet-stream, at most 50 MB. hash_id in every reply is the request's hash as lowercase hex in display order, the form explorers show.

Reply formats. Without extra headers, replies are flat objects keyed by hash_id (the plain format, what nodes before 1.2.3 speak and what share verification uses). With X-Verify-Result-Contract: scoped/1 the reply is keyed by its scope (network, operation, block, parent, height, ruleset, model, detector policy) and its scope_key, the SHA-256 of the scope's canonical JSON; the request must then carry height and ruleset. Use the scoped format for new block clients.

Headers on submit routes.

Header Value Notes
Content-Type application/octet-stream Required.
Accept application/json or absent A value that excludes JSON is 406.
X-Verify-Result-Contract scoped/1 Scoped reply format. Blocks only.
X-Target-Override 64 hex characters Share target. Full and Quick-Smell submit routes and their status routes; needs verify:share-mode. Not combinable with the scoped format.

Submit (asynchronous)

Return at once; poll for the result. Scope verify:invoke; Full also needs verify:full.

Route Check Where
POST /v1/verify/quick/request/submit Quick: proof-of-work, header, parameter bounds, token sequence CPU, seconds
POST /v1/verify/quick-smell/request/submit Quick plus a statistical test of the sampled tokens CPU, seconds
POST /v1/verify/full/request/submit Quick plus a replay of the model and a comparison of the logits GPU, about 10 s

Scoped format, new request: 202, header Retry-After (5 for Quick, 30 for Full).

{
  "status": "pending", "state": "queued", "verdict": null, "final": false,
  "hash_id": "6f1c…e2a9", "verification_type": "quick",
  "scope": {
    "scope_version": 1, "network": "tensor-main", "operation": "Quick",
    "subject": {"kind": "block", "hash_id": "6f1c…e2a9", "pow_blob_hash": "a3…17", "object_ref": null},
    "parent": "00000…4b2c", "height": 29301, "ruleset": 4,
    "model_identifier": "Qwen/Qwen3-8B@9c925d64d72725edaf899c6cb9c377fd0709d9c5",
    "detector": {"entry_digest": null, "state": "na"}, "target_mode": "block"
  },
  "scope_key": "5b0d…c7e1",
  "status_ref": "/v1/verify/result/5b0d…c7e1",
  "retry_after_seconds": 5,
  "request_generation": null
}

state is queued, running or needs_reverify (then an outcome object says why no verdict was produced). operation is Quick, Quick_Smell or Full.

Scoped format, result known: 200, the same body as GET /v1/verify/result/{scope_key}.

Plain format, new request: 202

{"status": "pending", "hash_id": "6f1c…e2a9", "verification_type": "full", "state": "accepted"}

(Quick-Smell answers {"status": "pending", "hash_id": "…"}.) Poll Quick with the batch route and verification_type: "quick"; Quick-Smell on GET /v1/verify/status/quick-smell/{hash_id}; Full on GET /v1/verify/status/full/{hash_id} or the batch route.

Plain format, result known: 200

{"status": "Full_Green", "hash_id": "6f1c…e2a9", "pow_blob_hash": "a3…17", "elapsed_ms": 10400, "cached": true}

With X-Target-Override (shares): the proof-of-work check uses the header value instead of the block target. A share above the target is answered 200 at once with "status": "Full_Red" (or Quick_Fail on Quick-Smell), "target_override_applied": true, "target_override_met": false and computed_header_hash. Otherwise the flow is as above and the result carries "target_override_applied": true and computed_header_hash. See Verifying shares.

503 with Retry-After: the request could not be taken now; retry.

Submit (synchronous)

Hold the connection until the verdict exists. Plain format only. Kept for older clients; prefer the asynchronous routes. The connection is closed after about five minutes; the backend answers 504 when its own budget runs out.

Route Scopes Statuses
POST /v1/verify/quick verify:invoke Quick_OK, Quick_Fail
POST /v1/verify/quick-smell verify:invoke Quick_*_Smell_*
POST /v1/verify/full verify:invoke, verify:full Full_Green, Full_Amber, Full_Red
POST /v1/verify/full/request verify:full as /v1/verify/full, without the options below

Options on the first three: ?details=true (or X-Verify-Details: true) asks for failure details; Content-Encoding: gzip is accepted (decompressed body under 50 MB). A known verdict is returned at once with "cached": true.

Results

Status reads never start work and are not counted against submit limits.

GET /v1/verify/result/{scope_key}

Scoped lookup. scope_key is 64 lowercase hex characters. verify:invoke, plus verify:full for a Full scope.

Reply Body
200 The result (below).
202, Retry-After Pending, same shape as the submit 202.
404 {"status": "unknown", "scope_key": "…"}: no result and no job. Submit again.
{
  "status": "Full_Green", "verdict": "Full_Green", "final": true,
  "hash_id": "6f1c…e2a9", "verification_type": "full",
  "scope": {"…": "operation Full"}, "scope_key": "9a4e…02bd", "request_generation": null,
  "policy": {"ruleset": 4, "model_identifier": "Qwen/Qwen3-8B@9c92…", "detector": {"entry_digest": null, "state": "na"}},
  "execution": {"executed_at": 1791325410.8, "elapsed_ms": 10400, "coalesced": false, "attempt": 1, "route": "scoped", "cached": true, "assurance": "unsampled"}
}

POST /v1/verify/status/batch

Several results in one call, Content-Type: application/json, body at most 1 MB. verify:invoke; verify:full if any item is full.

{
  "items": [
    {"hash_id": "6f1c…e2a9", "verification_type": "quick"},
    {"hash_id": "6f1c…e2a9", "verification_type": "full", "scope_key": "9a4e…02bd"}
  ],
  "wait_ms": 2000
}

verification_type is quick, quick_smell, full or pow. An item with scope_key is answered only by a result of exactly that scope; one without is a plain lookup by hash. wait_ms (0 to 10000) holds the call until the first item completes. Share results are not visible here.

{
  "completed": [
    {"status": "Quick_OK", "hash_id": "6f1c…e2a9", "verification_type": "quick", "elapsed_ms": 6410, "cached": true, "stored": true}
  ],
  "still_pending": [
    {"hash_id": "6f1c…e2a9", "verification_type": "full", "scope_key": "9a4e…02bd", "scope": {"…": "…"}, "state": "running", "final": false}
  ],
  "waited_ms": 2000,
  "server_ts": 1791325399.4
}

An item that is neither completed nor known appears in neither list. A plain quick item is also satisfied by a stored Full_Green for the same block (reported as Quick_OK).

GET /v1/verify/status/full/{hash_id}

A Full result by hash. verify:invoke and verify:full. 200 with the plain result, 202 with {"status": "pending", "state": "…", …}, or 404 {"detail": "No cached result found"}. With X-Target-Override it looks up the share result for that target. With ?scope_key=… and X-Verify-Result-Contract: scoped/1 it is the scoped lookup restricted to Full.

GET /v1/verify/status/quick-smell/{hash_id}

The same for Quick-Smell. verify:invoke.

GET /v1/verify/status/{hash_id}

Any-type lookup by hash: Full (with verify:full), then Quick-Smell, then other kinds; it does not find plain Quick results. verify:invoke.

Service

GET /v1/verifier/capabilities

What the service is serving right now, as the tensorcash.verifier.capabilities/1 document nodes from 1.2.3 read. Check serving[].operations before relying on a lane: only "state": "ready" is served.

{
  "schema": "tensorcash.verifier.capabilities/1",
  "service_version": "1.2.3",
  "network": "tensor-main",
  "rulesets": [0, 1, 2, 3, 4],
  "serving": [
    {
      "network": "tensor-main",
      "model_identifier": "Qwen/Qwen3-8B@9c925d64d72725edaf899c6cb9c377fd0709d9c5",
      "operations": {
        "Quick": {"state": "ready", "rulesets": [0, 1, 2, 3, 4]},
        "Quick_Smell": {"state": "unavailable", "reason": "smell_stats_missing"},
        "Full": {"state": "ready", "rulesets": [2, 3, 4]}
      }
    }
  ],
  "…": "…"
}

GET /v1/corrections

Verdicts that were changed after they were published, oldest first. ?since=<ISO 8601> (default: the whole 7-day window), ?limit= (1 to 5000, default 1000).

{
  "corrections": [
    {
      "hash_id": "6f1c…e2a9", "network": "tensor-main", "verification_type": "full",
      "request_context": "h=29301|rs=4|d=1234", "target_mode": "block", "pow_blob_hash": "a3…17",
      "original_status": "Full_Green", "corrected_status": "Full_Red", "corrected_at": 1791330000.0
    }
  ],
  "since": "2026-09-29T10:00:00+00:00", "retention_days": 7.0, "server_ts": 1791334800.2
}

A correction names one exact verification (request_context, target_mode block or share); later reads of that result return the corrected status.

GET /healthz

No key needed. {"status": "ok"} when the API is up.

Error shapes

Shape Where
{"detail": "…"} Most 4xx and 5xx
{"error": "…", "detail": "…"} / {"error": "…", "retry_after": n} 401 and 429
{"status": "refused" \| "rejected" \| "pending", "verdict": null, …} Scoped format refusals, rejections and no-verdict outcomes

Every error with its cause and action: Limits and errors.

Updated 2026-10-07