Routing explanations

GET /api/v1/responses/:id/explanation returns the decision card for a smart-routed run: exactly what the routing engine knew when it picked a runner, and — once the run is terminal — how the pick worked out. Explanations are recorded for routing: "smart" decisions; pinned runners, chains, and older runs return a 404 explanation_not_found. You don't need this endpoint to get the headline: every brain-routed response envelope (and response.completed / response.failed webhook payload) carries a compact banana_peel.routing_summary — runner, strategy, reason code, one redacted sentence, attribution, and the path back here for the full card.

curl -s "$BANANA_PEEL_BASE/api/v1/responses/resp_abc123/explanation" \
  -H "Authorization: Bearer $BANANA_PEEL_API_KEY"

The three sections

  • explanation — the decision-time snapshot: candidate table, chosen runner, structured reason, and the context that informed it.
  • decision — the dispatch plan derived from it: primary runner, ranked order, fallback pool, cost estimate and budget.
  • outcomenull until the run is terminal, then every attempt with fault attribution, reward, cost and timing, plus finalStatus / finalRunner.
{
  "object": "route_explanation",
  "response_id": "resp_abc123",
  "explanation": {
    "createdAt": "2026-08-18T14:02:11.482Z",
    "policyVersion": "bandit-v3",
    "featureSchemaVersion": "phi-v2",
    "slaProfile": "balanced",
    "domain": "example.com",
    "taskType": "extract",
    "candidates": [
      {
        "runner": "steel",
        "pSuccess": 0.87, "sampledPSuccess": 0.91, "pBlock": 0.04,
        "expCostUsd": 0.06, "expLatencyMs": 41000,
        "meanEu": 0.71, "sampledEu": 0.74,
        "priorLevel": "domain_task", "evidenceN": 23,
        "engineClass": "cdp", "proxyClass": "datacenter"
      }
      /* … one row per candidate the policy scored … */
    ],
    "chosen": "steel",
    "strategy": "exploit",
    "exploited": true,
    "reason": {
      "code": "exploit_expected_utility",
      "domain": "example.com", "taskType": "extract",
      "slaProfile": "balanced", "priorLevel": "domain_task", "evidenceN": 23
    },
    "fallbackPool": ["browserbase", "hyperbrowser", "skyvern"],
    "estimateUsd": 0.08, "budgetUsd": 2.5,
    "seededFrom": [],
    "siteProfile": {
      "domain": "example.com", "defense": "cloudflare",
      "cluster": "cf-managed", "observedFrom": "both"
    },
    "neighborsUsed": ["shop.example.org"],
    "appliedHints": ["hint_9f2"],
    "explorationQuota": { "targetMass": 12, "usedMass": 9.4, "remaining": 2.6 }
  },
  "decision": {
    "primary": "steel",
    "fallbackPool": ["browserbase", "hyperbrowser", "skyvern"],
    "ranked": ["steel", "browserbase", "hyperbrowser", "skyvern"],
    "strategy": "exploit",
    "estimateUsd": 0.08, "budgetUsd": 2.5
  },
  "outcome": {
    "finalStatus": "succeeded",
    "finalRunner": "steel",
    "attempts": [
      {
        "runner": "steel", "status": "succeeded",
        "attribution": "runner", "reward": 1,
        "costUsd": 0.05, "timingMs": 38000
      }
    ],
    "totalCostUsd": 0.05,
    "recordedAt": "2026-08-18T14:03:02.911Z"
  }
}

Candidate set

Every runner the policy scored appears in explanation.candidates with its expected-utility decomposition: pSuccess (posterior success probability), sampledPSuccess (the Thompson draw used for the decision), pBlock (block risk), expCostUsd and expLatencyMs, and the combined utilities meanEu / sampledEu. Each row also carries its evidence: priorLevel — how specific the history behind the estimate is (domain_task | domain | cluster_task | cluster | task | global) — and evidenceN, the weighted observation count behind it.

Explore vs. exploit

strategy and exploited say whether the router banked on a known winner or spent an exploration slot. reason.code is machine-readable: exploit_recent_winner, exploit_expected_utility, explore_cold_domain, explore_low_confidence, explore_floor, explore_streak_breaker, explore_budget_widening (the planned set exhausted with budget headroom left, and this candidate cleared the expected-value bar — the card gains a widening section with per-step EU tables and an honest stop reason such as widening_exhausted), forced_by_caller, or fallback_static_chain. explorationQuota exposes the per-domain exploration allowance (target mass, used mass, remaining) at decision time.

Site defense profile & transfer learning

siteProfile is the compact defense summary that informed the pick: detected defense (cloudflare | datadome | perimeterx | recaptcha | turnstile | akamai | none | unknown), its defense cluster, and whether it was observed from probes, real runs, or both. On cold domains, neighborsUsed lists neighbor domains whose learned stats were transferred to seed priors, and seededFrom records legacy aggregate seeding. appliedHints lists operator hint ids that shaped the decision (hard/soft steering, reward weights, guardrails).

Outcome & fault attribution

Once terminal, each attempt carries an attribution: runner | platform | environment | orchestration | task. Only runner-attributed outcomes update a runner's capability scores — platform outages, orchestration cancels, and impossible tasks are logged but never punish the runner. That keeps the benchmarks and future routing honest.

Policy versioning

Every decision records the policyVersion (and featureSchemaVersion) that produced it. New routing policies roll out in stages: shadow (scored on live decisions, never acted on) → A/B (a stable slice of traffic) → promote. Old decision cards stay interpretable because the version that made each call is stamped on the card.

Field reference

GET/api/v1/responses/{id}/explanation (getResponseExplanation)

Routing decision card for a smart-routed response

Parameters

FieldTypeReqDescription
idpath: stringyes

HTTP statuses

  • 200 — Route explanation (decision + outcome when terminal)
  • 404 — Unknown response id, or no explanation recorded for it

Generated from OpenAPI. Do not hand-maintain this table.

Fields are additive and optional — cards recorded before newer brain versions simply omit the newer fields. See also Smart routing for how the decision is made.

Command Palette

Search for a command to run...