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.outcome—nulluntil the run is terminal, then every attempt with fault attribution, reward, cost and timing, plusfinalStatus/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
| Field | Type | Req | Description |
|---|---|---|---|
| id | path: string | yes | — |
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.