Guides

Handle every verdict

Client code that branches correctly on all three outcomes and on the refusals around them.

The mistake to avoid is collapsing three different situations into one try/except. A refusal, an outage and a spent allowance need three different responses, and two of them should never reach a retry loop.

The shape

python
import httpx

def ask(intent: str, oracle: str = "chart") -> dict:
    r = httpx.post(
        f"{BASE}/v1/oracles/{oracle}/ask",
        headers={"Authorization": f"Bearer {KEY}"},
        json={"intent": intent},
        timeout=120,                       # a verdict can take this long
    )

    # ---- refusals: the request never reached an oracle -----------------
    if r.status_code == 401:
        # KEY_MISSING / KEY_UNKNOWN / KEY_REVOKED / KEY_EXPIRED.
        # A human has to act. Never retry.
        raise Fatal(r.json()["error"])
    if r.status_code == 402:
        # QUOTA_EXCEEDED. The credential is fine; the allowance is spent.
        # A different key will not help — quota is per organisation.
        raise OutOfCredits(r.json()["context"])
    if r.status_code == 404:
        raise Fatal("no such oracle — read GET /v1/oracles")
    if r.status_code == 422:
        # Includes "this oracle cannot produce that format", and the
        # response names the ones it can.
        raise Fatal(r.json())
    r.raise_for_status()

    # ---- verdicts: an oracle ran -------------------------------------
    answer = r.json()
    if answer["status"] == "error":
        return retry_with_backoff()        # ours, unbilled
    if answer["status"] == "invalid":
        return rejected(answer["detail"])  # billed, and a real answer
    return answer["result"]                # store verified_by with it

The distinctions that matter

402 is not 401. Your credential is fine. A client that treats a spent quota as an auth failure re-prompts a human for a key that was never the problem.

invalid is not an exception. It is the product working. Surface it to whoever asked, do not swallow it, and do not retry the identical intent hoping for a different roll.

error is the only thing worth retrying automatically, and it is free.

Store the citation

python
save(artifact=answer["result"], checked_against=answer["verified_by"])

An artifact without its verified_by is just a file. See Citations.

If an agent is the caller

The MCP tools do the transport half of this for you and hand your agent structured {"error": {"kind": …}} objects it can reason about.