# Flip API > Turn a plain-English outcome into a ready-to-sign, multi-step, cross-chain transaction > plan. Flip is non-custodial: it returns UNSIGNED transactions (EVM calldata + Solana > base64 messages); your own wallet or MPC signer signs and broadcasts. Flip never holds > keys, signs, or broadcasts. Base URL: https://app.fliplabs.ai/api/v1beta Auth: `Authorization: Bearer fk_...` (partner API key, kept server-side). Format: JSON in, JSON out, all public fields snake_case. Every response carries a `Flip-Schema-Version` header; every error uses one envelope: `{ error, friendly_message, issues[] }`. Metering: 5 credits per delivered call, debited from a per-tenant balance. ## What you are building Flip's product surface is a CHAT experience: prompt → streamed reasoning → clarifications → plan → sign. An integration is "Compose-in-a-box", not an API console. If you are a coding agent scaffolding a UI, follow the UI guidelines below — the default dashboard instinct (sidebars, raw JSON, a manual "Review plan" button) is wrong here. ## Flow (the happy path, automate all of it) 1. POST /intent — natural language in, structured `intents[]` out (or `clarifications[]` when ambiguous). Stream it (SSE). 2. When `intents[]` is non-empty, AUTOMATICALLY POST /plan — unsigned `steps[]` (real calldata) out. No "Review plan" click. 3. AUTOMATICALLY POST /simulate on the plan's same-chain step groups — render per-step results in the chat card BEFORE any wallet opens. 4. At sign time, POST /steps/reencode for the step being signed, then hand the FRESH calldata to the wallet. Quotes are perishable. Tier A integrations stop after step 3 (plan-only: prove the engine, never sign). Tier B integrations add a signer (MetaMask / Phantom / MPC) and follow the Signing rules below. ## Endpoints - POST https://app.fliplabs.ai/api/v1beta/intent: Parse a prompt into a structured plan. Body `{ prompt, wallets[], balances?, conversation_context?, clarify_follow_up?, options? }`. Send `Accept: text/event-stream` to stream as SSE — events: `delta` (reasoning text chunks), `reasoning`, `tool-start`, `tool-end`, `final`; the `final` event carries the full structured response `{ reasoning, suggested_name, intents[], clarifications[], kind }`. 5 credits. - POST https://app.fliplabs.ai/api/v1beta/plan: Resolve intents into unsigned transactions. Body `{ intents[], wallets[] }` (wallets should carry real on-chain balances for correct sizing). Returns `{ steps[], summary }`; each step is `{ order, type, chain, chain_id, source_wallet, to, data, value, description, ... }` (EVM) or carries `solana_tx` (Solana). Gas figures here are planner/encoder ESTIMATES, not a simulation. 5 credits. - POST https://app.fliplabs.ai/api/v1beta/steps/reencode: Re-quote ONE step at sign time. Body `{ step, actual_amount }` — `actual_amount` is a decimal number in token units (NOT wei): use `step.amount`; if the step is `chained`, use `step.chained.estimate_amount` (or the confirmed post-bridge amount once known). Never invent amounts from raw hex. Returns `{ ok, data, amount_raw, value, quoted_at }` or `solana_tx`; 409 `route_expired` when a pinned route is gone (re-plan). `quoted_at` marks when the quote was priced — open the wallet immediately, re-encode again if the user idles. - POST https://app.fliplabs.ai/api/v1beta/simulate: Dry-run steps against a fork before signing. Body `{ chain, sender, steps[] }` — one call per chain; `sender` is the user's signing wallet. Returns `{ success, gas_used, gas_price, gas_price_source, total_cost, error, steps[] }` with human-readable revert reasons in `error` / `steps[].error` (e.g. "ERC20: transfer amount exceeds balance") — show them directly in the chat card. Read-only. 5 credits. - GET https://app.fliplabs.ai/api/v1beta/usage: The tenant's credit balance and recent ledger. Returns `{ balance, cost_per_call, recent[] }`. Free. Partner/integrator metering — check the tenant balance and debug 402s server-side or in your partner ops dashboard. Not an end-user UI element: do not render a credits balance or refill CTA in the Compose chat. - GET https://app.fliplabs.ai/api/v1beta/meta: Capability discovery — `{ schema_version, cost_per_call, actions[], chains[], rate_tiers }`. Feature-detect from this; validate the actions/chains you render against it. Free. - POST https://app.fliplabs.ai/api/v1beta/agents: Create a agent — plain-English `{ request, wallets[] }` (compiled, may return `needs_clarification` or `422 unwatchable`) or typed `{ kind: price|yield|health|portfolio|schedule, wallets[], ... }` (no compile step). `wallets[]` are bare address strings, not `{address,chain}` objects. Not credit-metered; gated by a per-tenant active-agent quota (429 `agent_quota_exceeded` — beta 50 / standard 500 / high 5000). Phase A is notify-only. 201 (or 200 on `needs_clarification`). - GET https://app.fliplabs.ai/api/v1beta/agents: List the tenant's agents. Query `status` (active|paused), `kind` (price|yield|health|portfolio|schedule|condition), `wallet`, `cursor`, `limit`. Free. - GET/PATCH/DELETE https://app.fliplabs.ai/api/v1beta/agents/{id}: Get one; `PATCH { status: paused|active }` (any kind) or `PATCH { request }` (re-compile a `condition` agent's text only); `DELETE` is idempotent (204 whether or not it still existed). - POST https://app.fliplabs.ai/api/v1beta/agents/{id}/test: Manually trigger a agent once for integration testing — queues a `agent.test` event through the same delivery path a real trigger uses. 202. - GET https://app.fliplabs.ai/api/v1beta/agent-events: Cursor-paginated pull of the tenant's trigger history — the fallback/reconciliation rail when you haven't registered a webhook endpoint, or to replay a delivery gap. Delivery is at-least-once; dedup on `id`. Free. - POST/GET https://app.fliplabs.ai/api/v1beta/webhook-endpoints: Register (`{ url }`, https only, no private/internal hosts) or list webhook endpoints that agent triggers push to. `POST` returns `signing_secret` exactly once. Max 5 per tenant (429 `endpoint_quota_exceeded`). - DELETE https://app.fliplabs.ai/api/v1beta/webhook-endpoints/{id}: Remove a webhook endpoint. Idempotent (204). ## UI guidelines (make it feel like Flip) Layout - Single chat column, composer pinned at the bottom. Not a multi-panel admin layout. - Wallet connect stays compact (top-bar pills), never a full sidebar. - Reasoning, clarification chips, and the unsigned plan render INLINE in the thread, under the Flip message that produced them. - Never dump `intents[]` or step JSON in the primary UI. Structured payloads belong in logs/devtools. Calldata goes behind a disclosure, not a wall of hex. Flow - Stream /intent (SSE) into the Flip bubble as it arrives; show tool status lines while it thinks. - Clarifications render as option chips in the chat, then re-send with `clarify_follow_up` (template below) — never a modal form. - Auto-/plan when intents arrive; render each step as a readable card (title, chain, protocol, human description). Tone - Composer placeholder and empty state sound like chat ("Message Flip…"), with 2-3 example prompts as chips. Not an API console. Anti-patterns (do not ship these) - Two-column "Wallets | Compose" dashboards. - A required manual "Review plan" click after every intent. - Primary UI that is mostly OpenAPI-shaped JSON. - Hiding the plan until the user clicks something else. - A partner credits balance or "refill credits" control in the end-user chat UI — `/usage` is integrator metering, not end-user chrome. ## Signing (Tier B) - Execute `steps[]` in ascending `order`. Typical pattern: approve → wait for confirmation → swap. Soft-disable later Sign buttons until prerequisites confirm (allowance set, bridged funds arrived, `chained` amount known). Respect `wait_for_bridge` before simulating or signing post-bridge steps. This is UX guidance, not server-enforced — but out-of-order signing produces wallet failures and support tickets. - ALWAYS re-encode before signing: POST /steps/reencode, hand the returned calldata to the wallet, sign promptly. Re-encode again if the user idles past ~30s (`quoted_at` tells you the age). - HARD-FAIL on re-encode failure: if /steps/reencode returns an HTTP error or `ok: false`, do NOT fall back to plan-time calldata. Show `unsupported_reason` in the chat and stop. Plan-time calldata embeds quote-time amounts and deadlines; signing it stale can pass a simulation and still revert on-chain. - Trust Flip's errors over the wallet's: when calldata is stale or a swap would revert, browser wallets often mislabel it as "insufficient ETH for gas" / "likely to fail" even when gas is fine. Run /simulate and surface `steps[].error` in the chat card BEFORE opening the wallet; that message is the truth. - After a successful broadcast, show the tx hash + explorer link. Never imply Flip signed or broadcast anything. - Log Flip request/response bodies server-side (truncate calldata), plus a thin UI event log (prompt sent, clarification picked, sign clicked, re-encode result). That log is what makes support handoffs fast. ## Simulation pattern - Intended usage, same as Flip's own Compose UI: intent → (auto) plan → (auto) simulate → render per-step results before sign; re-simulate after a re-encode (results are only as fresh as the calldata you pass). - One /simulate call per chain (`chain` + `sender` + that chain's `steps`). - Multi-chain caveat: a batch simulates against CURRENT chain state. Steps funded by a bridge that hasn't landed will FALSELY revert if simulated pre-bridge — skip those with an honest "can't simulate until the bridge arrives" and simulate them after it lands. - Gas $ figures use real per-chain gas prices (`gas_price_source: "rpc"`); a `"fallback"` source means the $ estimate is approximate. ## Multi-turn conversations /intent is stateless; carry prior turns in the request field `conversation_context` (string): the prior turns newline-joined, one turn per line, each line `User: ` or `Flip: `, most recent last, ~8 turns max (the server truncates the block at 6000 chars). The current message goes in `prompt`, never in the block. Example conversation_context: User: what's the best stable yield on Base right now? Flip: Aave v3 USDC on Base pays the best stable rate across your chains right now. User: move my idle USDC there Flip: You hold 12,400 USDC idle on Ethereum. I'll bridge to Base and deposit into Aave v3. ## Clarifications When a request is ambiguous, /intent returns `intents: []` plus `clarifications[]`, each `{ id, kind, question, options: [{ id, label }] }`. Render the options as chips in the chat; when the user picks one, re-send /intent with: prompt = original prompt + "\n\n" + "Clarification — : " clarify_follow_up: true The flag makes the planner acknowledge the answer and continue — it never re-asks. Example: prompt: "deploy my idle USDC\n\nClarification — Which wallet should fund this?: Main treasury (Ethereum)" clarify_follow_up: true ## Wallets `wallets[]` entries are `{ address, chain }`. Include the chain the user intends to ACT on: if the connected EVM account will transact on Arbitrum, pass an entry with `chain: "arbitrum"` (or one entry per relevant chain) — a wallet passed only as "ethereum" while the prompt targets Arbitrum confuses planning. Pass real balances via `balances` when you have them; plans are sized from balances, and a missing balance can size a step to zero. ## Intent actions transfer, sweep, bridge, deposit, withdraw, stake, unstake, claim, approve, swap, borrow, repay, lp-add, lp-remove, lp-collect. `amount` is a token-denominated number, or the string `max` (full balance) or `chained` (size from the previous step's real output at sign time). `lp-add` uses `token0`/`token1`, `fee_tier` (Uniswap units, 3000 = 0.3%), `amount0`/`amount1`, optional `slippage_bps` — full-range positions only. ## Chains ethereum, base, arbitrum, optimism, polygon, robinhood, solana. EVM addresses are 0x-hex; Solana addresses are base58 (never lower-cased). ## Errors Single envelope `{ error, friendly_message, issues[] }`. Common codes: - 400 invalid_request — malformed body or unsupported chain/asset (`issues[]` lists offending fields). - 400 invalid_url — a webhook endpoint URL failed the safety check (not https, or a private/internal host). - 401 invalid_key — missing, malformed, or revoked key. - 402 insufficient_credits — tenant balance below the call cost. - 403 scope_forbidden — key not authorized for the endpoint. - 409 route_expired — a pinned route is gone; re-plan the step. - 422 unwatchable — a agent `request` isn't a measurable trigger; rephrase it. - 429 rate_limited — per-key tier exceeded; back off and retry. - 429 agent_quota_exceeded / endpoint_quota_exceeded — per-tenant agent or webhook-endpoint cap hit; pause/delete one first. ## Agents & webhook delivery Programmatic alerts (price / yield / health-factor / portfolio-move / schedule / a plain-English condition), scoped to your tenant, not credit-metered. Recommended setup order for an integration: register a webhook endpoint (`POST /webhook-endpoints`) → create a agent (`POST /agents`) → receive signed `POST` events at that endpoint. Registering an endpoint is optional, not required — a agent created with none registered still arms, evaluates, and triggers normally; it just has nothing to push to, so poll `GET /agent-events` instead (the same triggers, cursor-paginated). Every push is signed: `X-Flip-Signature: t=,v1=hex_hmac_sha256(signing_secret, ".")`, plus `X-Flip-Event-Id` (dedup key, at-least-once delivery, retried with backoff 1m/5m/30m/2h/12h before dead-lettering). Full walkthrough, typed-kind field reference, and a copy-paste signature-verification snippet: https://docs.fliplabs.ai/for-integrators/agents Agent ids: create, list, get, and the `agent.id` inside a trigger event ALL return the same public id, shaped `w__` (e.g. `w_alert_nn730k1p...`). Store it and pass it back verbatim on `/agents/{id}`, `/agents/{id}/test`, PATCH and DELETE. The prefix is the storage TYPE (alert | condition | reminder), NOT the public `kind` — a `kind:"price"` agent is `w_alert_...` — so you cannot derive the id from `kind`; use the id you were given. A bare, unprefixed id returns 404 agent_not_found. Wallets: Phase A agents are READ-ONLY (they observe and notify), so any address works — no signature, no connected session, no ownership check. `wallets[0]` is the owner for bookkeeping; array case is preserved (Solana is case-sensitive). Display: every agent carries a `rendered` one-liner for UI ("BTC price is above $65,350"); plain-English ones also carry `intent`. Prefer `rendered` over building your own label per kind. Clarification round-trip: a too-broad plain-English `request` returns 200 (NOT 201) with {status:"needs_clarification", question, options:[{id,label}]}. Both are 2xx — branch on the BODY (`status === "needs_clarification"`), never on res.ok, or you'll treat a clarification as a created agent. Send the option's LABEL back, not its id (ids are positional per response, nothing server-side remembers them); `options` may be empty (cap 4), in which case show a free-text input. Re-POST the ORIGINAL request with the answer folded in — `\n\nClarification — :