Candidate Lifecycle

The executable-handoff contract: search results become durable, opaque identities that downstream tools consume without re-specifying coordinates or filters. Co-designed with GPT-5.5 from its live evaluation of this API, 2026-07-11.

What a candidate_id represents

A candidate_id identifies a specific search result — not just a location. It binds the result to the exact data snapshot and search assumptions that produced it. It is opaque: never parse it, never construct one.

GET /api/v1/interconnection-queue/refined?min_mw=500&iso=MISO&geocoded_only=true

→ each survivor now carries:
  "candidate_id": "cand_7f9c5d2ab4e1f30a8c61",
  "snapshot_id":  "snap_2026-07-08",
  "expires_at":   "2026-07-18T18:00:00Z"

What snapshot_id guarantees

snapshot_id is immutable — it names the queue-data vintage (the load date of the underlying ISO disclosures). Two candidates sharing a snapshot were produced from identical underlying data. Re-running the same search against the same snapshot returns the same candidate ids.

First-mint-wins — read search_context as "originating search." Candidate identity is per (queue_id, snapshot_id), so if a different search already minted the same survivor within this snapshot, your search returns that existing identity — and resolve-candidate's search_context reflects the first search that minted it, not necessarily yours. Same underlying result = same identity across searches (this is also why re-searching can never extend a TTL). If your workflow needs your own filter context preserved, it belongs in your orchestration state, not the candidate. (Documented from Grok's first external audit, 2026-07-11.)

When ids expire

Identity and freshness are separate. A candidate expires 7 days after first mint — matching the refresh cadence of the underlying queue data, because a TTL longer than the data's own truth would be a lie. Re-searching does not extend a TTL.

What candidate_expired means

Fail closed, deterministically. Every consumer returns the identical error — no tool ever silently recomputes an expired candidate against newer data, because that would destroy reproducibility.
HTTP 410
{ "error": "candidate_expired",
  "message": "Re-run the search to obtain a fresh candidate." }

If an agent wants fresh data, it makes an explicit new search. That is the contract.

What every downstream tool echoes

Any response produced from a candidate carries the identity block, so an agent (or auditor) can reconstruct the chain of reasoning — including whether a difference came from newer data (search_version/snapshot) or a newer analysis implementation (analysis_version — they move independently):

"echo": {
  "candidate_id":        "cand_7f9c5d2ab4e1f30a8c61",
  "snapshot_id":         "snap_2026-07-08",
  "search_version":      "refined-queue/2026-07-11",
  "analysis_version":    "site-score/2026-07-11",
  "methodology_version": "composite-v2.3",
  "retrieved_at":        "2026-07-11T21:04:00Z",
  "citation": { "source": "DC Hub (dchub.cloud) — ISO interconnection queues + EIA",
                "as_of": "2026-07-08", "license": "CC-BY-4.0" }
}

Consuming a candidate

Tools accept candidate_id alongside (and in preference to) raw coordinates. When a candidate is given, location comes from the frozen mint — explicit lat/lon args are ignored, and no re-geocoding, re-ranking, or rounding occurs. capacity_mw stays caller-suppliable: it is your DC target, not candidate identity.

GET /api/site-score?candidate_id=cand_7f9c5d…&capacity_mw=50
POST /api/v1/rank-sites   {"candidates":[{"candidate_id":"cand_7f9c5d…",
                            "overall_score": 77.7}], "objectives":{…}}
GET /api/v1/resolve-candidate?candidate_id=cand_7f9c5d…

resolve-candidate is deliberately narrow: identity, location, snapshot metadata, provenance, and the originating search context. Never analysis, never a recompute — a dependable anchor for orchestration, not another analysis endpoint.

Recommended minimal rank_sites payload: {"candidate_id": "cand_…", <your enrichment metrics>} — e.g. overall_score from a site-score call. That's the whole contract: frozen identity fields (lat, lng, capacity_mw, fiber_km, iso) load from the mint automatically and cannot be overridden; anything else you attach is treated as an enrichment metric and is rankable. Do not re-send coordinates — that's the transcription-drift path this contract exists to close.

When to re-run search

Scope, honestly

v1 covers the REST rail: /api/v1/interconnection-queue/refined mints; /api/site-score, /api/v1/rank-sites, and /api/v1/resolve-candidate consume. The MCP tools (analyze_site, get_water_risk, rank_sites) accept candidate_id in the next server release. There is deliberately no automatic lineage graph: search creates, tools accept, every response echoes — the contract stays simpler than a workflow engine.

DC Hub (dchub.cloud) · Contract spec co-designed with GPT-5.5 (interactive evaluation, 2026-07-11); grounding deltas: real tool-surface mapping, snapshot = queue-load vintage, 7-day TTL. Questions: the full API surface is in /openapi.json · /llms.txt.