Download OpenAPI specification:Download
Turn a plain-English outcome into a ready-to-sign, multi-step, cross-chain transaction plan.
The Flip API exposes the same intent-to-transaction engine that powers the Flip app, as a small set of endpoints under a stable, versioned contract.
No endpoint ever holds keys, signs, or broadcasts. /plan returns UNSIGNED
transaction data — EVM to/data/value calldata and Solana base64 transaction
messages — that your own wallet or MPC signer executes. Flip has no signing
authority anywhere in the flow. The API decides what to do and how to encode
it; your signer decides whether to sign it.
POST /intent — natural language in, a structured plan out (the intents[]:
what to do, not yet how). May return clarifications instead when the
request is ambiguous.POST /plan — the intents[] in, unsigned transactions out (the steps[]:
real calldata your signer executes).POST /steps/reencode — re-quote a single step at sign time. Required for
correct integrations: chained and max amounts are re-sized from live
balances, and expiring routes (Jupiter blockhash, LiFi tool pinning) are
refreshed. Returns 409 when a route is gone and the step must be re-planned.POST /simulate (optional) — dry-run the steps[] against a fork before
signing to surface reverts and gas up front.Every request is authenticated with a partner API key as a bearer token
(Authorization: Bearer fk_...). Calls are metered against a per-tenant credit
balance: 5 credits per delivered call during beta. Check the balance any time
with GET /usage; feature-detect capabilities and limits with GET /meta.
Agents (programmatic alerts) and webhook delivery ship in this /v1beta API — see the
Agents and Webhook delivery tags below.
Wraps the app's /api/intent planner — same model routing, tools, and
clarification logic. Natural language in; a structured intents[] plan out
(or clarifications[] when ambiguous). Costs 5 credits per delivered call.
Multi-turn: pass prior turns in conversation_context — newline-joined
User: <text> / Flip: <text> lines, most recent last, ~8 turns max — with
the current message in prompt (see the followUp example).
Clarification round-trip: when clarifications[] comes back, render the
options[]; on pick, re-send with prompt = original prompt + "\n\n" +
Clarification — <question>: <picked label> and clarify_follow_up: true
so the planner acknowledges the answer and continues (see the
clarifyAnswer example).
Send Accept: text/event-stream to stream the build as Server-Sent Events;
otherwise a single JSON body is returned once planning completes.
| Accept | string Default: application/json Enum: "application/json" "text/event-stream" Set to |
| prompt required | string [ 1 .. 2000 ] characters The desired outcome in plain English. |
required | Array of objects (Wallet) non-empty The wallets in play — at least one. |
Array of objects (Balance) Optional pre-fetched holdings. Omit and Flip fetches them. | |
| conversation_context | string <= 6000 characters Prior turns of the conversation, for multi-turn planning: the prior turns
newline-joined, one turn per line, each line |
| clarify_follow_up | boolean Set |
object Coarse run options. Raw model ids are never exposed. |
{- "prompt": "move my idle USDC into the best stable yield across my chains",
- "wallets": [
- {
- "address": "0x8f2a...d5",
- "chain": "ethereum"
}
]
}{- "reasoning": "You hold 12,400 USDC idle on Ethereum. Aave v3 on Base is paying the best stable rate right now, so I'll bridge to Base and deposit.",
- "suggested_name": "Idle USDC → best stable yield",
- "kind": "transactional",
- "intents": [
- {
- "action": "bridge",
- "asset": "USDC",
- "from_chain": "ethereum",
- "to_chain": "base",
- "amount": "max"
}, - {
- "action": "deposit",
- "protocol": "aave",
- "asset": "USDC",
- "chain": "base",
- "amount": "chained",
- "pool_id": "aave-v3-base-usdc",
- "vault_address": "0xa238dd80..."
}
], - "clarifications": [ ]
}Wraps the app's /api/intent-plan resolver — trusted-vault gating, Pendle-PT
expansion, route prefetch, feasibility checks, and the repair pass are all
inherited. Takes the intents[] from /intent (or hand-built to the same
shape) plus the signing wallets[] with their on-chain balances, and returns
the ordered steps[] of unsigned transactions. Costs 5 credits per call.
required | Array of objects (Intent) non-empty The |
required | Array of objects (Wallet) non-empty Signing wallets, each ideally carrying the on-chain |
{- "intents": [
- {
- "action": "transfer",
- "asset": "USDC",
- "chain": "base",
- "from_chain": "string",
- "to_chain": "string",
- "from_asset": "string",
- "to_asset": "string",
- "from": "string",
- "to": "string",
- "spender": "string",
- "protocol": "string",
- "pool_id": "string",
- "vault_address": "string",
- "amount": "max",
- "min_amount": 0,
- "slippage_bps": 1,
- "sources": [
- {
- "chain": "string",
- "wallet": "string"
}
], - "destination": {
- "chain": "string",
- "wallet": "string"
}
}
], - "wallets": [
- {
- "address": "0x8f2a...d5",
- "chain": "ethereum",
- "label": "string",
- "balances": [
- {
- "symbol": "USDC",
- "amount": 12400,
- "usd_value": 0,
- "chain": "string"
}
]
}
]
}{- "steps": [
- {
- "order": 1,
- "type": "bridge",
- "chain": "ethereum",
- "protocol": "LiFi",
- "action_type": "Bridge",
- "asset": "USDC",
- "amount": 12400,
- "to": "0x1231deb6...",
- "data": "0x4630a0d8...",
- "value": "0",
- "description": "Bridge 12,400 USDC Ethereum → Base via LiFi",
- "wait_for_bridge": true
}, - {
- "order": 2,
- "type": "deposit",
- "chain": "base",
- "protocol": "aave",
- "action_type": "Deposit",
- "asset": "USDC",
- "amount": 0,
- "to": "0xa238dd80...",
- "data": "0x617ba037...",
- "value": "0",
- "description": "Supply USDC to Aave v3 on Base",
- "chained": {
- "from_key": "0x8f2a...d5@base@USDC",
- "estimate_amount": 12391.4,
- "min_output": 12300
}
}
], - "summary": {
- "total_after_fees": "12,391.4 USDC",
- "bridge_count": 1,
- "est_fee_usd": "3.10"
}
}Wraps the app's /api/re-encode-step. Re-encodes one step against the
real amount available at sign time — required for correct integrations:
chained and max amounts are re-sized from live balances, and expiring
routes (Jupiter blockhash, LiFi tool pinning) are refreshed so the user signs
the same aggregator and fee structure they were quoted. Returns 409 with
error: route_expired when a pinned route is gone and the step must be
re-planned via /plan.
required | object (Step) One unsigned transaction. EVM steps carry |
| actual_amount required | number The real amount, in token units (not wei), to re-encode the step for. |
{- "step": {
- "type": "deposit",
- "asset": "USDC",
- "chain": "base",
- "protocol": "aave",
- "to": "0xa238dd80...",
- "data": "0x617ba037..."
}, - "actual_amount": 12391.4
}{- "ok": true,
- "data": "0x617ba037...updated",
- "amount_raw": "12391400000",
- "value": "0"
}Wraps the app's simulation layer (Tenderly for EVM, a Solana simulate for
Solana). Executes the steps[] against a fork and returns per-step success,
status, and gas so you can surface reverts and cost before anything is signed.
Read-only — nothing is broadcast. Costs 5 credits per call.
Calldata-bearing steps are pre-validated against the target address: a step that CALLS an address with no deployed code (dead or self-destructed contract) returns 400 before any simulation runs. Plain value transfers to EOAs are exempt and simulate normally.
| chain required | string Enum: "ethereum" "base" "arbitrum" "optimism" "polygon" "solana" The chain the steps execute on — one simulate call per chain. EVM slugs dispatch to the Tenderly fork; |
| sender required | string The address the simulation runs FROM (your user's signing wallet). EVM: required 0x-hex. Solana: applied to any step missing its own |
required | Array of objects (Step) non-empty The |
{- "chain": "arbitrum",
- "sender": "0x0036534C48a754163F0f446601f36BCfdA840f57",
- "steps": [
- {
- "order": 0,
- "type": "transfer",
- "chain": "string",
- "chain_id": 0,
- "protocol": "string",
- "action_type": "string",
- "asset": "string",
- "from_chain": "string",
- "to_chain": "string",
- "amount": 0,
- "amount_raw": "string",
- "to": "string",
- "data": "string",
- "value": "string",
- "description": "string",
- "estimated_gas": "string",
- "bridge_fee": 0,
- "bridge_time": "string",
- "wait_for_bridge": true,
- "chained": {
- "from_key": "string",
- "estimate_amount": 0,
- "min_output": 0
}, - "pool_id": "string",
- "solana_tx": "string",
- "solana_format": "legacy",
- "solana_error": "string",
- "slippage": 0,
- "fee_bps": 0,
- "swap_tool": "string",
- "error": "string",
- "success": true
}
]
}{- "success": true,
- "gas_used": "246,000",
- "gas_price": "0.02 gwei",
- "gas_price_source": "rpc",
- "total_cost": "$0.02",
- "error": null,
- "steps": [
- {
- "success": true,
- "status": "✅",
- "status_code": 200,
- "action": "Approve USDC for Kyberswap",
- "action_type": "Approve",
- "asset": "USDC",
- "gas": "36,138",
- "error": null
}, - {
- "success": true,
- "status": "✅",
- "status_code": 200,
- "action": "Swap 1 USDC for ETH",
- "action_type": "Swap",
- "asset": "USDC",
- "gas": "209,862",
- "error": null
}
]
}The current per-tenant credit balance, the flat per-call cost, and the most recent metered calls. Reads are free (not metered). This is integrator/partner metering — for checking balances and debugging 402s in your own ops, not an end-user UI element. Don't render a credits balance or refill CTA in the end-user Compose chat.
{- "balance": 4820,
- "cost_per_call": 5,
- "recent": [
- {
- "kind": "spend",
- "amount": 5,
- "reason": "api_intent",
- "at": 1751909204000
}
]
}Machine-readable capability discovery: the current schema version, the flat per-call credit cost, the supported intent actions, the supported chains, and the rate-limit tiers. Feature-detect from this rather than hard-coding. Free.
{- "schema_version": "2026-07-15",
- "cost_per_call": 5,
- "actions": [
- "transfer",
- "sweep",
- "bridge",
- "deposit",
- "withdraw",
- "stake",
- "unstake",
- "claim",
- "approve",
- "swap",
- "borrow",
- "repay",
- "lp-add",
- "lp-remove",
- "lp-collect"
], - "chains": [
- "ethereum",
- "base",
- "arbitrum",
- "optimism",
- "polygon",
- "robinhood",
- "solana"
], - "rate_tiers": {
- "beta": {
- "max_requests": 30,
- "window_ms": 60000
}, - "standard": {
- "max_requests": 120,
- "window_ms": 60000
}, - "high": {
- "max_requests": 600,
- "window_ms": 60000
}
}
}Programmatic alerts (price / yield / health-factor / portfolio-move / schedule / a plain-English condition) scoped to your tenant. Not metered by credits — gated by a per-tenant active-agent quota (by rate tier) and the standard per-key rate limit.
Send either a plain-English body { request, wallets } or a typed body
{ kind, wallets, ... } — never both, never neither (400 otherwise). Not
metered by credits; gated instead by a per-tenant active-agent quota (429 agent_quota_exceeded once the tier cap is hit — beta 50 / standard 500 /
high 5000).
Plain-English ({ request, wallets }) runs the same compile pipeline the
Flip app and Telegram bot use. Three outcomes:
201 with agent + compile.201,
created as a kind: schedule agent.200 with status: needs_clarification, a question, and up to
4 options. Re-submit by sending request again with the picked option
folded in: "<original request>\n\nClarification — <question>: <picked label>" — the same inline convention /intent's clarification round-trip
uses, just folded into the next request string (there's no multi-turn state
to carry here).422 unwatchable.Phase A is notify-only. Even when the compiler recognizes a "when X, do Y" request with a real follow-on action, the agent is armed as notify-only and the action is dropped — action agents over the API are a later phase.
Typed ({ kind, wallets, ... }) maps 1:1 onto the internal create paths for
price / yield / health / portfolio / schedule — no compile step, no LLM
call, always 201. kind: "condition" is not a typed-create kind — it only
results from the plain-English path.
For kind: "price", coingecko_id is required in the request — this endpoint
does not resolve a ticker symbol to a CoinGecko id for you; look it up first
(e.g. CoinGecko's /coins/list) and pass the id directly (e.g. "ethereum",
not "ETH").
Fires are delivered to any webhook endpoints you've registered
(POST /webhook-endpoints) — recommended, so you don't have to poll. With none
registered, a agent still arms, evaluates, and triggers normally; retrieve its
triggers from GET /agent-events instead.
| request required | string [ 3 .. 500 ] characters |
| wallets required | Array of strings (AgentWallets) [ 1 .. 10 ] items EVM |
{- "request": "notify me when ETH is above 5000",
- "wallets": [
- "0x8f2a...d5"
]
}{- "status": "needs_clarification",
- "question": "Which \"gas\" — Ethereum L1 gwei, or a specific chain?",
- "options": [
- {
- "id": "opt_0",
- "label": "Ethereum L1 gas (gwei)"
}, - {
- "id": "opt_1",
- "label": "Base gas (gwei)"
}
]
}Tenant-scoped, newest-first, offset-paginated. Both status and kind filter
against the public vocabulary shown on the returned agent objects (not
the internal per-table storage) — see the callouts on each parameter below.
| status | string Enum: "active" "paused" Filters on the public status (see the |
| kind | string Enum: "price" "yield" "health" "aave-health" "spark-health" "portfolio" "news" "condition" "schedule"
|
| wallet | string Scope to agents whose wallet set includes this address (case-insensitive). |
| cursor | string Opaque pagination cursor from a previous response's |
| limit | integer [ 1 .. 200 ] Default: 50 |
{- "agents": [
- {
- "id": "w_condition_k17f2a9c",
- "type": "alert",
- "kind": "price",
- "status": "active",
- "created_at": 0,
- "last_checked_at": 0,
- "last_fired_at": 0,
- "wallets": [
- "0x8f2a...d5"
], - "condition": "above",
- "threshold": 0,
- "asset_symbol": "string",
- "coingecko_id": "string",
- "pct_change": 0,
- "base_price": 0,
- "pool_id": "string",
- "protocol": "string",
- "chain": "string",
- "asset": "string",
- "metric": "apy",
- "target_wallet": "string",
- "movement_window": "24h",
- "rendered": "string",
- "fire_headline": "string",
- "cadence": "hourly",
- "intent": "string",
- "label": "string",
- "hour": 0,
- "minute": 0,
- "day_of_week": 0,
- "day_of_month": 0,
- "timezone": "string"
}
], - "next_cursor": "string"
}| id required | string Example: w_condition_k17f2a9c The public agent id, e.g. |
{- "agent": {
- "id": "w_condition_k17f2a9c",
- "type": "alert",
- "kind": "price",
- "status": "active",
- "created_at": 0,
- "last_checked_at": 0,
- "last_fired_at": 0,
- "wallets": [
- "0x8f2a...d5"
], - "condition": "above",
- "threshold": 0,
- "asset_symbol": "string",
- "coingecko_id": "string",
- "pct_change": 0,
- "base_price": 0,
- "pool_id": "string",
- "protocol": "string",
- "chain": "string",
- "asset": "string",
- "metric": "apy",
- "target_wallet": "string",
- "movement_window": "24h",
- "rendered": "string",
- "fire_headline": "string",
- "cadence": "hourly",
- "intent": "string",
- "label": "string",
- "hour": 0,
- "minute": 0,
- "day_of_week": 0,
- "day_of_month": 0,
- "timezone": "string"
}
}Send either { status } or { request }.
{ status: "paused" | "active" } works identically on any agent kind — the
public vocabulary is always active/paused regardless of what's stored
internally per kind (see the Agent.status schema note). An id that's
well-formed but doesn't resolve to a row you own (already deleted, or another
tenant's) is 404, not 500.
{ request: "<new plain-English text>" } only works on kind: "condition"
agents (400 on any other kind — delete and recreate a typed agent
instead). Re-runs the same compile pipeline POST /agents uses and mirrors
its outcomes 1:1: ready → 200 { agent } (in place — same id, trigger history
reset, matching the app's own edit-a-condition-agent behavior); ambiguous →
200 { status: "needs_clarification", ... }; not watchable → 422 unwatchable.
| id required | string Example: w_condition_k17f2a9c The public agent id, e.g. |
| status required | string Enum: "paused" "active" |
{- "status": "paused"
}{- "agent": {
- "id": "w_condition_k17f2a9c",
- "type": "alert",
- "kind": "price",
- "status": "active",
- "created_at": 0,
- "last_checked_at": 0,
- "last_fired_at": 0,
- "wallets": [
- "0x8f2a...d5"
], - "condition": "above",
- "threshold": 0,
- "asset_symbol": "string",
- "coingecko_id": "string",
- "pct_change": 0,
- "base_price": 0,
- "pool_id": "string",
- "protocol": "string",
- "chain": "string",
- "asset": "string",
- "metric": "apy",
- "target_wallet": "string",
- "movement_window": "24h",
- "rendered": "string",
- "fire_headline": "string",
- "cadence": "hourly",
- "intent": "string",
- "label": "string",
- "hour": 0,
- "minute": 0,
- "day_of_week": 0,
- "day_of_month": 0,
- "timezone": "string"
}
}A structurally-invalid id (doesn't match the w_<type>_<id> shape) is 404.
Idempotent: a well-formed id whose row is already gone (e.g. a repeated
delete, or a retry after a timed-out first attempt) returns 204 as a no-op —
it never distinguishes "already gone" from "just deleted" in the response.
| id required | string Example: w_condition_k17f2a9c The public agent id, e.g. |
{- "error": "invalid_key",
- "friendly_message": "Missing or malformed API key. Pass it as `Authorization: Bearer fk_...`.",
- "issues": [ ]
}Queues a synthetic trigger through the exact same delivery pipeline a real trigger
takes (webhook push + the GET /agent-events pull rail both see it),
flagged type: "agent.test" — independent of whether the agent's real
trigger is currently met. 202 because the event is queued into the same
delivery sweep a real trigger uses, not returned inline; it typically lands
within the next delivery-cron tick (every 2 minutes).
| id required | string Example: w_condition_k17f2a9c |
{- "value": {
- "ok": true,
- "note": "test event will be delivered like a real trigger, flagged test:true"
}
}Tenant-scoped trigger history, newest-first, cursor-paginated on triggered_at.
This is how a partner without a registered webhook endpoint reconciles triggers,
and how any partner replays or backfills around a delivery gap. Delivery
(both this rail and the webhook push rail below) is at-least-once — dedup
on id regardless of which rail an event was read from.
Note: this rail and the pushed webhook body (see the webhooks section)
are built from the same event mapping — id, type, agent, and the set of
trigger fields are identical for the same trigger on both rails. The one
intentional difference is the timestamp: this rail returns created_at as an
integer epoch-ms, while the pushed webhook body returns created as an
ISO-8601 string, for the same instant. Both rails' trigger objects are
snake_case (e.g. value_at_trigger), so a handler can parse either
rail's event with the same code.
| cursor | string Opaque pagination cursor from a previous response's |
| limit | integer [ 1 .. 200 ] Default: 50 |
{- "events": [
- {
- "id": "evt_k7m2ab91",
- "type": "agent.triggered",
- "created_at": 1753142400000,
- "agent": {
- "id": "k17f2a9c",
- "kind": "condition"
}, - "trigger": {
- "headline": "ETH crossed above $5,000",
- "kind": "condition",
- "condition": "above",
- "threshold": 0,
- "wallet": "0x8f2a...d5"
}
}
], - "next_cursor": null
}Register an HTTPS endpoint that agent triggers are pushed to, and pull/replay trigger
history. See the webhooks section below for the delivery contract (signing,
retries, dead-lettering).
Sent to every active endpoint registered for your tenant whenever a agent
of yours triggers (including test triggers from POST /agents/{id}/test, which
carry type: "agent.test"). Delivery is at-least-once — dedup on the
payload's id (identical to the X-Flip-Event-Id header), never assume
single delivery.
Acknowledge with any 2xx status within 10 seconds. Anything else —
non-2xx, a timeout, or a redirect (redirects are never followed) — counts as a
failed attempt and is retried with backoff: 1m → 5m → 30m → 2h → 12h after
attempts 1 through 5, then the delivery is dead-lettered (no further
retries for that specific trigger). An endpoint that racks up 10 consecutive
dead-lettered deliveries is auto-disabled (GET /webhook-endpoints will show
status: "disabled") — re-register or fix your receiver and the next trigger
goes to a healthy endpoint again.
Structurally identical to the GET /agent-events pull-rail's event shape
for the same trigger — id, type, agent, and the set of trigger fields all
come from the same event mapping. Two differences to know about:
created (an ISO-8601 string); the pull rail's
equivalent is created_at (an integer epoch-ms) — same instant, different
field name and format.snake_case keys inside trigger (e.g. value_at_trigger),
matching every other public object in this API.In both rails, agent.id is the SAME public w_<type>_<id> value returned
by create/list/get — pass it straight back to /agents/{id},
/agents/{id}/test, PATCH or DELETE without rebuilding it. Note the
prefix is the storage type (alert | condition | reminder), NOT the public
kind: a kind: "price" agent is w_alert_..., so kind alone is not
enough to construct the id — which is why the event carries it. It is identical
across both rails for the same trigger, including manual test-triggers
(POST /agents/{id}/test) of every kind.
| X-Flip-Event-Id required | string Example: evt_k7m2ab91 Identical to the payload's |
| X-Flip-Signature required | string Example: t=1753142400,v1=5f6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e
|
| id required | string |
| type required | string Enum: "agent.triggered" "agent.degraded" "agent.test" |
| created required | string <date-time> ISO-8601. (The pull rail's equivalent field is |
required | object |
required | object Same field set as |
| property name* additional property | any |
{- "id": "evt_k7m2ab91",
- "type": "agent.triggered",
- "created": "2026-07-30T18:40:00.000Z",
- "agent": {
- "id": "k17f2a9c",
- "kind": "condition"
}, - "trigger": {
- "headline": "ETH crossed above $5,000",
- "kind": "condition",
- "condition": "above",
- "threshold": 0,
- "wallet": "0x8f2a...d5"
}
}https:// only; the hostname can't be a raw private/loopback/link-local IP
literal (including the 169.254.169.254 cloud metadata address),
localhost/*.localhost, or *.local/*.internal — 400 invalid_url
otherwise. This is a hostname/IP-literal check only; a public hostname that
later resolves to a private address is re-checked at every send, but not
pinned against rebinding between check and connect.
Maximum 5 endpoints per tenant — exceeding it is 429 endpoint_quota_exceeded. Remove an old endpoint first if you're unsure how
many you have registered (GET /webhook-endpoints).
The response's signing_secret is shown exactly once — store it now. It
is not derived from anything you can look up later; only its hash is kept
server-side. See the webhooks section for how to verify it against inbound
deliveries.
| url required | string <= 2048 characters |
{
}{- "signing_secret": "whs_3f9a7b2e1c4d8f6a0b5e9c2d7a1f4b8e6c0d3a9f7b2e5c1d8a4f0b6e3c9d7a2f"
}Never includes secrets — only the POST response ever carries the raw signing_secret.
{- "endpoints": [
- {
- "id": "string",
- "url": "string",
- "status": "active",
- "disabled_reason": "string",
- "created_at": 0
}
]
}Idempotent: an id that doesn't resolve to a row you own (already removed, or
another tenant's) still returns 204 as a no-op.
| id required | string Example: k9c7f2ab |
{- "error": "invalid_key",
- "friendly_message": "Missing or malformed API key. Pass it as `Authorization: Bearer fk_...`.",
- "issues": [ ]
}Send Accept: text/event-stream to /intent (or options.stream = true) to stream
the plan as it is built — the same event set the app renders: delta and reasoning
(prose as it is written), tool-start / tool-end (render "thinking" status lines),
and a terminal final event carrying the structured intents / clarifications.
Omit the header for a single buffered JSON response.
/intent is stateless — carry prior turns yourself in the conversation_context
request field. Join the prior turns with newlines, one turn per line, each line
User: <text> or Flip: <text>, most recent last, ~8 turns max (the server
truncates the block at 6000 characters). The current message always goes in
prompt, never in the block. Example follow-up request:
prompt: "actually only move half of it"
conversation_context: "User: move my idle USDC into the best stable yield\nFlip: You hold 12,400 USDC idle on Ethereum. Aave v3 on Base pays the best stable rate, so I'll bridge to Base and deposit."
When a request is ambiguous ("which wallet funds this?", "which Morpho market?"),
/intent returns intents: [] and a populated clarifications[] — each
{ id, kind, question, options: [{ id, label }] }. Render the options to your
user; on pick, re-send /intent with clarify_follow_up: true and prompt set
to the original prompt + "\n\n" + the plain-text answer block
Clarification — <question>: <picked label>. The flag makes the planner
acknowledge the answer and continue — it never re-raises the same clarification.
Example re-send:
prompt: "deploy my idle USDC\n\nClarification — Which wallet should fund this?: Main treasury (Ethereum)"
clarify_follow_up: true
Public fields are snake_case and stable. Internal app routes may change freely;
this /v1beta surface is the frozen public contract, echoed on every response via
the Flip-Schema-Version header. Breaking changes ship under a new version prefix;
additive changes bump the dated schema_version in place.
2026-07-15 — additive: intent actions lp-add, lp-remove, lp-collect
(Uniswap liquidity positions, full-range); chain robinhood; /simulate may
now return 400 when a calldata-bearing step targets an address with no
contract code (dead/self-destructed target detection). Doc correction:
SimulateRequest now documents the live shape (chain + sender +
steps — the previously published steps+wallets shape was never
what the API accepted). Fix: /steps/reencode no longer fails
same-chain swap re-quotes with LiFi error 1011; when a pinned route can
no longer fill, reencode now retries once unpinned instead of hard-
failing; responses carry quoted_at; default slippage is floored for
small stable-input swaps ($1 → 3%, ≥$5 → unchanged 1%) so dust swaps
stop missing their minimum output during the signing window.
2026-07-07 — initial beta contract.