# Banana Peel — full agent guide Product: Banana Peel — smart routing for browser agents. Base: https://bananapeel.com OpenAPI: https://bananapeel.com/openapi.json Agents docs: https://bananapeel.com/docs/agents Agent-native signup: https://bananapeel.com/docs/agents/signup MCP docs: https://bananapeel.com/docs/mcp Skill: https://bananapeel.com/agent-skill ## No credentials — agent-native signup If the human has no Banana Peel account, do NOT ask them to sign up first and do NOT send them to /keys. 1. Check for BANANA_PEEL_API_KEY / BANANAPEEL_API_KEY (whichever is set) or ~/.bananapeel/credentials. If a key exists, reuse it. Do not mint another account. 2. If absent, follow /docs/agents/signup: run `npx -y @banana-peel/cli init --agent --json`. Banana Peel detects your agent environment and configures itself. Stdout is only status, claim_url, has_existing, and configured — never the API key. Never paste the key into chat. If npx/the CLI is unavailable, use the supported HTTP fallback (challenge → signup) documented on /docs/agents/signup. Command: `npx -y @banana-peel/cli init --agent --json` (stores key in ~/.bananapeel/credentials, mode 0600). Banana Peel detects your agent environment and configures itself. 3. Never ask the human to create an account manually. Console /keys is an optional advanced path for humans who already have an account — not step 1 for agents. **Never paste the API key into chat.** Only share claim_url with the human. Same $10 welcome credits as Console. Docs: https://bananapeel.com/docs/agents/signup HTTP fallback (supported): If `npx`/the CLI is unavailable in your environment, use the HTTP flow directly: GET /api/v1/agent-signup/challenge → solve the returned instruction → POST /api/v1/agent-signup with {challenge_id, challenge_answer}. The response contains api_key (shown once; unlike CLI stdout it passes through your context — write it to ~/.bananapeel/credentials (mode 0600) or env immediately and never echo it) and claim_url. Exact shapes: https://bananapeel.com/docs/agents/signup and the llms.txt signup section. ## MCP Prefer the official stdio MCP server. Banana Peel detects Claude Code, Cursor, Codex, VS Code, Windsurf, and Claude Desktop and writes MCP config when those environments are present. https://bananapeel.com/docs/mcp - create_response → POST /api/v1/responses - get_response → GET /api/v1/responses/:id (Optional wait: true polls until completed, failed, or requires_action.) - submit_input → POST /api/v1/responses/:id/input - cancel_response → POST /api/v1/responses/:id/cancel - list_runs → GET /api/v1/runs - list_rankings → GET /api/v1/rankings - list_capabilities → GET /api/v1/capabilities Package: `npx -y @banana-peel/mcp` with BANANA_PEEL_API_KEY set. When a target site asks for MFA, 2FA, or an OTP, the run pauses immediately and asks the user for the code. It does not keep trying other runners. CAPTCHA and other bot-detection gates (reCAPTCHA, hCaptcha, Turnstile, DataDome, Cloudflare challenge, “prove you are human”) are solved, re-routed, or end blocked with reason bot_challenge. They never become a human text prompt. MFA via MCP: create_response waits until requires_action (or terminal). When human_action_required is true, ASK THE HUMAN IN CHAT NOW for the site MFA/OTP code. Call submit_input with their code. Do not retry the task or pick another runner. Then get_response(wait:true) to terminal. Submit the code with POST /api/v1/responses/{id}/input { "code": "123456" } (or { "answer": "…" } for text/confirm). MCP: submit_input({ id, code }). Then keep polling until completed or failed. If the vault credential (or request totp_secret) includes a base32 TOTP seed, OTP asks are answered in-process and the run does not pause. Username/password alone is not enough — the run still pauses for the code. Docs: https://bananapeel.com/docs/guides/mfa ## What it is Banana Peel is an OpenAI Responses-compatible HTTP API. You send a browser task plus `routing`. Banana Peel selects a runner from 250+ runners (Browserbase, Steel, Browser Use, Skyvern, Hyperbrowser, Playwright, Deck, …), executes the run, optionally falls back, and returns a Responses-shaped JSON object with details under `banana_peel`. `routing` is what you requested; `banana_peel.runner` is the runner that executed. Deck Agent is one runner among many. Pin it with routing:"deck" only when you specifically want it; otherwise prefer routing: smart. ## Runs vs. sessions - Run: a unit of work. "Do this task" → routed to a runner → result with attempts, cost, duration, verdict. The Responses API object; billed per executed attempt; benchmarks and smart routing learn from run outcomes. - Session: a live browser you hold open. Create it on a session surface (/api/v1/sessions Browserbase-shaped, /api/wrappers/steel/v1/sessions Steel-shaped), connect over CDP with Playwright/Puppeteer, drive it yourself, release it. No task known to Banana Peel; a single concrete browser, so never smart-routed. - Shorthand: runs are what you asked for; sessions are what you're holding. - banana_peel.session on a run is metadata about the browser that run used (live view / replay / connect URL) — not a standalone session you manage. - Docs: https://bananapeel.com/docs/concepts/sessions ## Authentication If there is no API key, follow the golden path on https://bananapeel.com/docs/agents / https://bananapeel.com/docs/agents/signup: `npx -y @banana-peel/cli init --agent --json`. Check ~/.bananapeel/credentials or env BANANA_PEEL_API_KEY (alias BANANAPEEL_API_KEY) first. `npx -y @banana-peel/cli whoami --json` resolves account_id via GET /api/v1/me (env or file key; never prints the secret). Never ask the human to create an account. Console https://bananapeel.com/keys is optional for humans who already have an account. Headers (either works on Browser Agent API and wrappers): - Authorization: Bearer bp_live_… - x-api-key: bp_live_… Browserbase SDK may send X-BB-API-Key; Banana Peel accepts Banana Peel keys there for the sessions wrapper. Env convention: ``` export BANANA_PEEL_BASE=https://bananapeel.com export BANANA_PEEL_API_KEY=bp_live_… ``` ## Create a run — POST /api/v1/responses URL: https://bananapeel.com/api/v1/responses Timeout: allow up to ~420s (Cloud Run). Request fields (generated from OpenAPI — https://bananapeel.com/openapi.json). Agents should always send `routing` (prefer smart). `credential_id` is injected at run time onto the runner selected by routing; it does not imply Deck. ### POST /v1/responses (`createResponse`) Create a browser-agent response Request body: - `model` (string, optional, default: "banana-peel") - `input` (string | array | object, optional) — Task text (string, OpenAI content array, or structured {url, goal, success_criteria, steps}). Required unless `url` alone is enough with goal/task. - `url` (string (uri), optional) - `instructions` (string, optional) - `goal` (string, optional) - `task` (string, optional) - `credential_id` (string, optional) — Optional vault credential id for the API key owner (create/list via /v1/credentials). After custody is satisfiable, secrets are decrypted and injected into the task for whichever runner routing selects (smart, steel, browserbase, …). Vault usage never pins routing to deck (deck_only is an opt-in custody policy; create default is trusted_runners). When routing does select Deck — pinned, smart, race, or learning — the credential is additionally attached through Deck’s native vault (registered per source, reused across runs), since Deck gates login-walled sources on an attached credential rather than the instruction text. - `stream` (boolean, optional) — OpenAI Responses SSE stream (text/event-stream). Takes precedence over background for this request. - `background` (boolean, optional) — Return 202 immediately and finish in the background. Preferred for human MFA/OTP relay (poll + POST …/input). - `metadata` (object, optional) — OpenAI metadata (string values); echoed on the response object. - `allow_interaction` (boolean, optional) — Human-in-the-loop. Default on (response id is the relay channel). false opts out; true refuses non-relay runners. - `interaction_timeout_ms` (integer, optional) — Max wait per MFA/OTP/text question (default 240000). - `totp_secret` (string, optional) — Base32 TOTP secret — OTP asks answered in-process (prefer vault credential_id). - `totp_identifier` (string, optional) — Skyvern-native TOTP inbox/phone identifier. - `totp_url` (string, optional) — Skyvern-native TOTP poll URL. - `normalize` (boolean, optional) — Paid output normalization (+$0.10 / run). Requires output_schema (or text.format / response_format schema). Build schemas in Console → Normalize. - `output_schema` (object, optional) — Desired output JSON Schema (or property map). Used when normalize is true or when a schema is supplied via text.format / response_format. HARD CONTRACT: a terminal success must populate exactly these keys (required keys present, non-empty, right type; no undeclared extra data keys unless additionalProperties: true — run metadata like url/final_url is tolerated). A run whose output cannot satisfy the schema terminates status:failed with error.code schema_violation and the nonconforming output kept under banana_peel.output for inspection; the normalization add-on is not billed on violation. Without a schema, read the stable default envelope instead: banana_peel.answer / answer_present / evidence. - `success_criteria` (string, optional) — Explicit success criterion for the quality grader (e.g. "an order number is present"). - `steps` (string[], optional) — Optional ordered steps for structured task input. - `credentials_ref` (string, optional) — Alias for credential_id (vault credential). - `custody` (string, optional) — Credential custody policy for THIS request: any | trusted_runners | deck_only | pinned:[,…]. Hard-filters the runner candidate set before any routing / scoring / fan-out; when no runner survives, the request fails fast with 409 `custody_unsatisfiable` and nothing is billed. Use with inline credentials in the task text; vault credentials carry their own stored policy (both compose as AND). Enforcement is echoed under `banana_peel.custody`. - `text` (object, optional) — OpenAI Responses text.format passthrough (json_schema → normalize). - `response_format` (object, optional) — OpenAI-style response_format (json_schema / json_object). - `idempotency_key` (string, optional) — Client retry key — equivalent to the `Idempotency-Key` HTTP header (send either; if both, they must match or the create fails with 400 `idempotency_key_mismatch`). Honored on sync, background (`background: true` / Prefer: respond-async), and streaming create; keys are per-workspace, shared across every run-creating endpoint, and expire 24h after first use. The key is reserved before dispatch: reuse with an identical body (`stream`/`background` excluded from the comparison) replays the existing run (original `created_at`, `Idempotent-Replayed: true` header) instead of starting a duplicate; reuse with a different body is refused with 422 `idempotency_conflict`; a duplicate racing the original create gets 409 `idempotency_in_flight`. - `routing` (string | string[], optional, default: "smart") — Optional (default smart). Learning and learning-all exist to teach the router the best path: they calibrate the routing brain on a domain/task so later smart runs pick the winning runner. smart = production (router uses the brain to pick/fallback) | learning = paid calibration: probe the curated live ~54-runner pool on this task/URL (not the default for production traffic) | learning-all = paid thorough calibration: full-catalog sweep (250+ options, NO early stop; most expensive; do not use for every production job) | named runner (browserbase, steel, browser-use, skyvern, hyperbrowser, playwright, deck, …) | category MVP pool (SOC2/HIPAA/EU — fixed runner lists, not attestation) | ordered runner list. Learning probes batches of 9 (concurrency 8) and stops after the first wave containing a success; learning-all keeps the same waves but sweeps the full catalog (250+ options): every dispatchable option attempts once; coming-soon catalog rows are skipped (never billed); $20 default ceiling (a catalog-wide sweep typically hits it). Learning-family runs are paid calibration sweeps (executed attempts bill); winners ranked fastest+cheapest feed future smart routing. - `sla_profile` (string, optional, enum: balanced | cheapest | fastest | most_reliable, default: "balanced") — Smart-routing preference: how the router weighs cost, speed, and block-avoidance against success probability when choosing runners. Only affects routing: "smart". - `max_fallbacks` (integer, optional, default: 5) — Smart routing: how many additional runners after the primary (1 + N). Default 5 (up to 6 attempts unless spend, time, or a breaker stops earlier). 0 pins to the first pick only. Invalid values fail free with 400 invalid_max_fallbacks. Only affects routing: "smart" (and category). Staggered race still starts the primary first. - `max_cost_usd` (number, optional) — Per-run spend ceiling (USD), honored by every routing mode. Once executed attempts’ spend reaches the ceiling, no NEW attempts / batches / waves are dispatched; in-flight attempts finish (and bill) and the run ends with error code `budget_exhausted` plus partial results under `banana_peel.routing_attempts`. Learning runs default to $20 when omitted (an explicit max_cost_usd overrides the default in either direction). The active ceiling is echoed under `banana_peel.budget` and reflected by POST /v1/estimate. Response body (note: HTTP can be 200 even when status is "failed" — always check status): - id, object:"response", status: completed|failed|in_progress|requires_action|cancelled - required_action: present when status is requires_action (submit_input / otp|text|confirm) - output_text - banana_peel: canonical banana_peel.run/v1 (`runner` = executed runner, ranked_runners, timing_ms, cost, session, artifacts, fallback_from, …) - banana_peel.progress: while in flight — attempts so far, Learning batch, spend_usd Fetch later: GET /api/v1/responses/:id Delete: DELETE /api/v1/responses/:id — purges copied artifacts immediately; subsequent GET is 404 (cross-account also 404) Cancel: POST /api/v1/responses/:id/cancel (executed attempts bill; nothing new after) Submit MFA/OTP: POST /api/v1/responses/:id/input body: {"code":"123456"} List: GET /api/v1/runs?limit=50 (no cursor pagination yet) Webhooks (prefer at production scale): POST /api/v1/webhooks {url, events?} — at-least-once signed events response.completed|failed|requires_action|cancelled; dedupe event.id; sequence per response. Docs: https://bananapeel.com/docs/api/webhooks ## Data retention Copied screenshots, downloads, and artifact blobs are kept **7 days** by default, then dropped. Provider session-replay URLs are stored for the same window but the video itself follows the provider TTL (we do not copy replay media). Enterprise can set 24h / 7d / 30d via PATCH /api/v1/workspace; zero-retention is a contract term (we skip persisting blobs). Benchmarks use success/cost/latency aggregates — not page content. Docs: https://bananapeel.com/docs/security/retention ## MFA / OTP (full) When a target site asks for MFA, 2FA, or an OTP, the run pauses immediately and asks the user for the code. It does not keep trying other runners. CAPTCHA and other bot-detection gates (reCAPTCHA, hCaptcha, Turnstile, DataDome, Cloudflare challenge, “prove you are human”) are solved, re-routed, or end blocked with reason bot_challenge. They never become a human text prompt. Poll GET /api/v1/responses/:id until status is requires_action (or handle the response.requires_action webhook). MCP create_response waits until that pause and returns human_action_required plus required_action. Submit the code with POST /api/v1/responses/{id}/input { "code": "123456" } (or { "answer": "…" } for text/confirm). MCP: submit_input({ id, code }). Then keep polling until completed or failed. If the vault credential (or request totp_secret) includes a base32 TOTP seed, OTP asks are answered in-process and the run does not pause. Username/password alone is not enough — the run still pauses for the code. Cursor / MCP: ASK THE HUMAN IN CHAT NOW for the site MFA/OTP code. Call submit_input with their code. Do not retry the task or pick another runner. Docs: https://bananapeel.com/docs/guides/mfa ## Routing semantics Learning and learning-all exist to teach the router the best path: they calibrate the routing brain on a domain/task so later smart runs pick the winning runner. Run learning (or once, learning-all) on a new domain/task to teach the brain, then switch back to smart. Do not use learning-all for every production job. smart: - Production: the router uses the brain to pick a runner and fall back. - Ranks runners from live success history for the target domain - Prefers a prior Learning ranking (fastest+cheapest winners) when one exists for the source — that is the point of a prior learning / learning-all run - After the first pick, up to `max_fallbacks` additional runners (default 5; 0 pins to the first choice). Staggered race starts the primary first. - Default chain when cold: browserbase → steel → hyperbrowser → browser-use → skyvern → anchor → deck → playwright learning: - Purpose: teach the router the best path on this domain/task (not production traffic) - Paid calibration: probe the curated live pool (~54) on this task/URL so the brain learns which runners win. $20 default ceiling. Not the default for production traffic. - Broader discovery than smart: evaluate the live operational runner pool (~54 base runners) in batches of 9 - Inside each batch, at most 8 runners run at once (quota-safe) - If the whole batch fails for blocked/capacity/hang reasons, advance to the next 9 - Stops after the first wave containing a success (cheaper bootstrap) - Hard task failures stop probing (do not burn the rest of the pool) - Among successes, rank by fastest + cheapest → #1 primary, #2/#3 fallbacks for that domain - Persists to source prefs used by future `smart` requests - Requires a resolvable target URL (400 missing_url otherwise — fails free, nothing bills) - Create with background:true, then poll banana_peel.progress (current batch, attempts, spend) and cancel any time with POST /api/v1/responses/:id/cancel - Paid calibration sweep: executed attempts bill (same as smart fan-out — not success-only); $20 default ceiling (max_cost_usd overrides) - See https://bananapeel.com/docs/routing/learning learning-all: - Purpose: teach more thoroughly than learning — still not a production mode - Paid thorough calibration: sweep the full dispatchable catalog (250+), more expensive, $20 ceiling. Do not use for every production job. - Full-catalog sweep (250+ options: bases + distinct driver×model combos) with **NO early stop** — every dispatchable option attempts exactly once regardless of successes; the best success (fastest+cheapest) is the run result, with the complete per-runner attempt table - Catalog-only coming-soon options are skipped (never billed) — banana_peel.learning.skipped_no_adapter and banana_peel.learning.pool show catalog vs dispatchable vs attempted - Same wave machinery, admission caps (per-account Learning concurrency), async 202 contract, and missing_url validation as learning - Health-suspended runners are skipped honestly (never attempted or billed) — see banana_peel.learning.skipped_suspended - Only the spend ceiling, cancel, or hard TTL can cut the sweep short (terminal budget_exhausted with partial attempt table; a catalog-wide sweep typically hits the $20 default — echo says stopped early (budget) after N of M) - Paid calibration sweep: $20 default ceiling (env-overridable; max_cost_usd overrides per request) - When to use: teach a domain thoroughly ONCE to seed the scoreboard; prefer learning for a cheaper teach, smart for everyday traffic. Do not use learning-all for every production job. - See https://bananapeel.com/docs/routing/learning named runner: - Pin one of: browserbase, steel, browser-use, skyvern, hyperbrowser, anchor, deck, playwright, auto - Still may surface fallbacks depending on execution path ordered chain: - e.g. ["browserbase","steel","skyvern"] — try in order categories (MVP): - SOC2, HIPAA, EU map to fixed runner pools today - NOT full SOC2/HIPAA/EU attestation or residency enforcement - See https://bananapeel.com/docs/routing/categories custody (enforced): - Vault credentials carry a custody policy (any | trusted_runners | deck_only | pinned:); default on create is trusted_runners — deck_only is opt-in, not implied by using the vault - Secrets are injected at run time onto the runner routing selects (same as inline username/password). Vault usage never pins routing to deck; when routing DOES select Deck, the credential is additionally attached through Deck's native vault (registered per source, reused across runs) because Deck gates login-walled sources on an attached credential - Requests may add `custody` for inline credentials - Enforced as a HARD pre-filter on the runner candidate set before any routing/scoring/fan-out — no fallback, race, wave, or Learning probe leaves the allowed set - Policy vs routing conflict → 409 custody_unsatisfiable BEFORE dispatch (fails free, nothing bills) - Responses echo `banana_peel.custody` ({ policy, source, credential_id, allowed_runners, allowed_runner_count, candidates_before_filter, enforced }) and persist it on the run - See https://bananapeel.com/docs/security/custody ## Output normalization (+$0.10) Optional post-run reshape into your JSON schema. Enable with `normalize: true` + `output_schema` (JSON Schema or property map), or OpenAI `text.format` / `response_format` with a schema. Console builder: https://bananapeel.com/normalize. Flat +$0.10 on top of runner cost + platform fee — charged only when the pass actually executes. The response echoes the outcome under `banana_peel.normalization` (`{ requested, applied, reason, addon_usd }`; `applied: false` → reason set, addon_usd 0). `missing_output_schema` if normalize is on without a schema. Estimate via `POST /api/v1/estimate`. Full docs + example: https://bananapeel.com/docs/api/responses#normalize ## Credits & billing - New accounts receive $10 welcome credits - Each run debits: runner listed price + 5% platform fee (0% until your first top-up) - Output normalization add-on: +$0.10 flat, billed only when the pass runs (see `banana_peel.normalization.applied`) - Failed, blocked, and parallel/Learning attempts that executed are billed - `learning` / `learning-all` are paid calibration sweeps to teach the router the best path — then use `smart`. `learning` can attempt ~54 live runners (typically one wave) — expect high spend bounded by $20; `learning-all` considers the full catalog (250+ options, highest spend, $20 default — usually budget-stops). Do not use learning-all for every production job. - Balance exhausted → HTTP 402, code=insufficient_credits, hint points to Console → Billing - Console: https://bananapeel.com/billing ## Error recovery (agents) Always parse JSON body.error when HTTP is 4xx/5xx (and sometimes inspect run failure on 200/502). Envelope: ``` { "error": { "message": "…", "type": "authentication_error|invalid_request_error|…", "code": "invalid_api_key|invalid_routing|missing_input|insufficient_credits|…", "param": "routing"|null, "doc_url": "https://bananapeel.com/docs/agents", "hint": "optional recovery tip" } } ``` Recovery map: - invalid_api_key → `npx -y @banana-peel/cli init --agent --json` (or set Bearer / x-api-key from ~/.bananapeel/credentials); if npx is unavailable, use the HTTP fallback (GET /api/v1/agent-signup/challenge → POST /api/v1/agent-signup, see https://bananapeel.com/docs/agents/signup); do not send the human to /keys first - invalid_routing → send routing: smart or a known slug/chain - missing_input → provide input and/or url - missing_output_schema → pass output_schema (or text.format / response_format) with normalize; build at /normalize - invalid_custody → use custody: any | trusted_runners | deck_only | pinned:[,…] - custody_unsatisfiable (409) → credential custody policy excludes every runner in the requested routing; loosen the policy (Console → Vault or PATCH /api/v1/credentials/:id) or route within banana_peel.custody.allowed_runners (fails free — nothing billed) - credential_not_found → use a credential id owned by this API key; list with GET /api/v1/credentials or create with POST /api/v1/credentials - insufficient_credits → top up billing - response_not_found → wrong id or other workspace - Run status failed with HTTP 200/502 → read error / banana_peel.error; retry with smart or alternate routing chain Do NOT treat console dispatcher codes (saturated, coming_soon on POST /api/live/run) as the public API contract. ## Compatibility wrappers Prefer POST /api/v1/responses for new work. If migrating an existing SDK (docs: https://bananapeel.com/docs/wrappers): - Browserbase → baseURL https://bananapeel.com/api , sessions at /v1/sessions (package: packages/wrappers/browserbase) - Steel → /api/wrappers/steel/… (package: packages/wrappers/steel; sessions alias /v1/sessions) - Browser Use → /api/wrappers/browser-use/… (package: packages/wrappers/browser-use) - Skyvern → /api/wrappers/skyvern/… (package: packages/wrappers/skyvern) - Hyperbrowser → /api/wrappers/hyperbrowser/… (package: packages/wrappers/hyperbrowser) There is no /api/wrappers/browserbase (404). Wrapper packages live under packages/wrappers when you already use a provider SDK. ## Out of scope / traps - /api/v2/* mock store (tasks/task-runs) — ignore for product integrations - POST /api/live/run — internal console path - evidence_level — not enforced on public Browser Agent API yet (custody IS enforced; see https://bananapeel.com/docs/security/custody) - Category pools ≠ compliance certification ## Integration checklist 1. If no key: `npx -y @banana-peel/cli init --agent --json`. Else set BANANA_PEEL_BASE and BANANA_PEEL_API_KEY 2. Prefer MCP (https://bananapeel.com/docs/mcp) in Cursor/Claude; else Load OpenAPI https://bananapeel.com/openapi.json 3. POST /api/v1/responses (or create_response) with routing: smart and a clear input+url (use background:true for MFA) 4. Branch on HTTP error.code / hint / doc_url 5. On success, read status, output_text, banana_peel.runner (executed runner), banana_peel.ranked_runners 6. Persist response id; GET /api/v1/responses/:id (or get_response) if needed 7. If status is requires_action → POST /api/v1/responses/:id/input (or submit_input) with the code, then keep polling 8. Use wrappers only when preserving an existing provider SDK 9. Monitor credits via console billing ## One-pagers - cURL / TypeScript / Python: https://bananapeel.com/docs/agents - MCP: https://bananapeel.com/docs/mcp - Examples download: https://bananapeel.com/examples/curl.sh, https://bananapeel.com/examples/create-response.ts, https://bananapeel.com/examples/create_response.py - Thin client helper: https://bananapeel.com/examples/bananapeel-client.ts ## Links - https://bananapeel.com/llms.txt - https://bananapeel.com/llms-full.txt - https://bananapeel.com/openapi.json - https://bananapeel.com/openapi.yaml - https://bananapeel.com/docs/mcp - https://bananapeel.com/.well-known/ai-plugin.json - https://bananapeel.com/robots.txt