openapi: 3.1.0
info:
  title: Flip API
  version: 1beta
  summary: >-
    Turn a plain-English outcome into a ready-to-sign, multi-step, cross-chain
    transaction plan.
  description: >
    # Overview


    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.


    ## Non-custodial by 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.


    ## The flow: intent, plan, sign


    1. **`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.
    2. **`POST /plan`** — the `intents[]` in, unsigned transactions out (the
    `steps[]`:
       real calldata your signer executes).
    3. **`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.
    4. **`POST /simulate`** *(optional)* — dry-run the `steps[]` against a fork
    before
       signing to surface reverts and gas up front.

    ## Authentication & metering


    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.
  contact:
    name: Flip Labs
  x-schema-version: '2026-07-15'
servers:
  - url: https://app.fliplabs.ai/api/v1beta
    description: Production (beta)
security:
  - PartnerKey: []
tags:
  - name: Planning
    description: Turn intent into signable transactions.
  - name: Execution
    description: Re-quote and simulate steps at sign time.
  - name: Account
    description: Usage, credit balance, and capability discovery.
  - name: Agents
    description: >
      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.
  - name: Webhook delivery
    description: >
      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).
  - name: Streaming (SSE)
    x-traitTag: true
    description: >
      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.
  - name: Multi-turn conversations
    x-traitTag: true
    description: >
      `/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."
  - name: Clarifications
    x-traitTag: true
    description: >
      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
  - name: Versioning
    x-traitTag: true
    description: >
      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.
paths:
  /intent:
    post:
      operationId: createIntent
      tags:
        - Planning
      summary: Parse a natural-language prompt into a structured plan
      description: >
        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.
      parameters:
        - $ref: '#/components/parameters/AcceptStream'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            examples:
              idleYield:
                summary: Move idle USDC into the best stable yield
                value:
                  prompt: >-
                    move my idle USDC into the best stable yield across my
                    chains
                  wallets:
                    - address: 0x8f2a...d5
                      chain: ethereum
              followUp:
                summary: A multi-turn follow-up carrying the prior turns
                value:
                  prompt: actually only move half of it
                  conversation_context: >-
                    User: move my idle USDC into the best stable yield across my
                    chains

                    Flip: 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.
                  wallets:
                    - address: 0x8f2a...d5
                      chain: ethereum
              clarifyAnswer:
                summary: Re-send after the user picked a clarification option
                value:
                  prompt: >-
                    deploy my idle USDC


                    Clarification — Which wallet should fund this?: Main
                    treasury (Ethereum)
                  clarify_follow_up: true
                  wallets:
                    - address: 0x8f2a...d5
                      chain: ethereum
      responses:
        '200':
          description: >
            The structured plan. With `Accept: text/event-stream` the body is an
            SSE

            stream (see the `text/event-stream` content below); otherwise a
            single

            JSON `IntentResponse`.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntentResponse'
              examples:
                plan:
                  summary: A two-step bridge-then-deposit plan
                  value:
                    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: []
                clarify:
                  summary: Ambiguous request → a clarification instead of intents
                  value:
                    reasoning: >-
                      You've got USDC ready to deploy. Pick which wallet should
                      fund this and I'll build the plan on the next pass.
                    suggested_name: Deploy idle USDC
                    kind: transactional
                    intents: []
                    clarifications:
                      - id: source_wallet
                        kind: wallet
                        question: Which wallet should fund this?
                        options:
                          - id: 0x8f2a...d5
                            label: Main treasury (Ethereum)
                          - id: 0x41bc...9a
                            label: Ops wallet (Base)
            text/event-stream:
              schema:
                $ref: '#/components/schemas/IntentStreamEvent'
              examples:
                stream:
                  summary: A trimmed SSE transcript
                  value: >
                    event: reasoning

                    data: {"text":"You hold 12,400 USDC idle on Ethereum."}


                    event: tool-start

                    data: {"name":"getYields"}


                    event: tool-end

                    data: {"name":"getYields"}


                    event: final

                    data: {"reasoning":"...","suggested_name":"Idle USDC → best
                    stable
                    yield","kind":"transactional","intents":[{"action":"bridge","asset":"USDC","from_chain":"ethereum","to_chain":"base","amount":"max"}],"clarifications":[]}
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/EngineError'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /plan:
    post:
      operationId: createPlan
      tags:
        - Planning
      summary: Resolve intents into unsigned, ready-to-sign transactions
      description: >
        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.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlanRequest'
      responses:
        '200':
          description: The ordered list of unsigned transaction steps.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanResponse'
              examples:
                steps:
                  summary: Bridge + deposit, unsigned
                  value:
                    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'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/EngineError'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /steps/reencode:
    post:
      operationId: reencodeStep
      tags:
        - Execution
      summary: Re-quote a single step at sign time
      description: >
        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`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReencodeRequest'
            examples:
              chainedDeposit:
                summary: Re-encode a chained deposit with the real bridged amount
                value:
                  step:
                    type: deposit
                    asset: USDC
                    chain: base
                    protocol: aave
                    to: 0xa238dd80...
                    data: 0x617ba037...
                  actual_amount: 12391.4
      responses:
        '200':
          description: The freshly encoded transaction data for the step.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReencodeResponse'
              examples:
                ok:
                  value:
                    ok: true
                    data: 0x617ba037...updated
                    amount_raw: '12391400000'
                    value: '0'
                unsupported:
                  summary: >-
                    Step kind can't be re-encoded — surface the reason and stop
                    (hard-fail)
                  value:
                    ok: false
                    unsupported_reason: swap re-encode requires a fresh /plan
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '409':
          $ref: '#/components/responses/RouteExpired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /simulate:
    post:
      operationId: simulateSteps
      tags:
        - Execution
      summary: Dry-run steps against a fork before signing
      description: >
        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.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimulateRequest'
      responses:
        '200':
          description: Per-step simulation results plus an aggregate.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulateResponse'
              examples:
                ok:
                  value:
                    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
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/EngineError'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /usage:
    get:
      operationId: getUsage
      tags:
        - Account
      summary: Return the tenant's credit balance and recent ledger
      description: >
        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.
      responses:
        '200':
          description: Balance and recent usage.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
              examples:
                usage:
                  value:
                    balance: 4820
                    cost_per_call: 5
                    recent:
                      - kind: spend
                        amount: 5
                        reason: api_intent
                        at: 1751909204000
        '401':
          $ref: '#/components/responses/InvalidKey'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /meta:
    get:
      operationId: getMeta
      tags:
        - Account
      summary: Discover schema version, supported actions, chains, and limits
      description: >
        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.
      responses:
        '200':
          description: Capability and version metadata.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetaResponse'
              examples:
                meta:
                  value:
                    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
        '401':
          $ref: '#/components/responses/InvalidKey'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /agents:
    post:
      operationId: createAgent
      tags:
        - Agents
      summary: Create a agent — plain-English or typed
      description: >
        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:

        - A concrete, measurable condition → `201` with `agent` + `compile`.

        - A schedule phrase ("every day at 9am...", no measurable condition) →
        `201`,
          created as a `kind: schedule` agent.
        - Ambiguous → `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).
        - Not a watchable condition, or refused by an internal match-gate (the
        compiled
          check doesn't faithfully match what was asked) → `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.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentCreateRequest'
            examples:
              plainEnglish:
                summary: Plain-English — a measurable price condition
                value:
                  request: notify me when ETH is above 5000
                  wallets:
                    - 0x8f2a...d5
              plainEnglishSchedule:
                summary: Plain-English — a schedule phrase (no measurable condition)
                value:
                  request: check in on my portfolio every weekday morning at 9am
                  wallets:
                    - 0x8f2a...d5
              typedPrice:
                summary: Typed — price above a threshold
                value:
                  kind: price
                  wallets:
                    - 0x8f2a...d5
                  asset_symbol: ETH
                  coingecko_id: ethereum
                  condition: above
                  threshold: 5000
              typedHealth:
                summary: Typed — Aave health factor below a threshold
                value:
                  kind: health
                  wallets:
                    - 0x8f2a...d5
                  protocol: aave
                  chain: base
                  threshold: 1.5
              typedSchedule:
                summary: Typed — a weekly check-in, explicit timezone
                value:
                  kind: schedule
                  wallets:
                    - 0x8f2a...d5
                  cadence: weekly
                  hour: 9
                  minute: 0
                  day_of_week: 1
                  timezone: America/New_York
      responses:
        '200':
          description: >-
            Ambiguous plain-English request — resolve the clarification and
            re-submit.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NeedsClarificationResponse'
              examples:
                clarify:
                  value:
                    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)
        '201':
          description: The created agent.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentCreateResponse'
              examples:
                condition:
                  summary: Plain-English condition, compiled and armed
                  value:
                    agent:
                      id: w_condition_k17f2a9c
                      type: condition
                      kind: condition
                      status: active
                      created_at: 1753142400000
                      wallets:
                        - 0x8f2a...d5
                      intent: notify me when ETH is above 5000
                      rendered: ETH price is above $5,000
                      cadence: hourly
                    compile:
                      rendered: ETH price is above $5,000
                      fire_headline: null
                      cadence: hourly
                typedPrice:
                  summary: Typed price agent
                  value:
                    agent:
                      id: w_alert_k9d81ab2
                      type: alert
                      kind: price
                      status: active
                      created_at: 1753142400000
                      wallets:
                        - 0x8f2a...d5
                      condition: above
                      threshold: 5000
                      asset_symbol: ETH
                      coingecko_id: ethereum
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '422':
          $ref: '#/components/responses/Unwatchable'
        '429':
          description: >-
            Either the per-key rate limit or the active-agent quota was
            exceeded.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rateLimited:
                  value:
                    error: rate_limited
                    friendly_message: Rate limit exceeded for this key. Slow down and retry.
                    issues: []
                quotaExceeded:
                  value:
                    error: agent_quota_exceeded
                    friendly_message: >-
                      This tenant already has 50 active agents — the "beta" tier
                      limit. Pause or delete one before creating another.
                    issues: []
        '503':
          $ref: '#/components/responses/NotConfigured'
    get:
      operationId: listAgents
      tags:
        - Agents
      summary: List agents
      description: >
        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.
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - active
              - paused
          description: >-
            Filters on the public status (see the `Agent.status` schema note) —
            matches regardless of which internal table/kind a agent lives in, so
            `status=paused` correctly surfaces a paused agent of any kind, not
            just `schedule` ones.
        - name: kind
          in: query
          schema:
            type: string
            enum:
              - price
              - yield
              - health
              - aave-health
              - spark-health
              - portfolio
              - news
              - condition
              - schedule
          description: >-
            `health` matches both `aave-health`- and `spark-health`-kind rows
            (the same aggregate value shown in each agent's own `kind` field) —
            this is the filter value to use. `aave-health`/`spark-health` are
            also accepted individually if you want just one protocol's health
            agents. `news` agents can appear in a listing but cannot be created
            through this API.
        - name: wallet
          in: query
          schema:
            type: string
          description: >-
            Scope to agents whose wallet set includes this address
            (case-insensitive).
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque pagination cursor from a previous response's `next_cursor`.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: A page of agents.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentListResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /agents/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: The public agent id, e.g. `w_condition_k17f2a9c`.
        example: w_condition_k17f2a9c
    get:
      operationId: getAgent
      tags:
        - Agents
      summary: Get one agent
      responses:
        '200':
          description: The agent.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentGetResponse'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
    patch:
      operationId: updateAgent
      tags:
        - Agents
      summary: Pause/resume a agent, or recompile a condition agent's text
      description: >
        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`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentPatchRequest'
            examples:
              pause:
                summary: Pause a agent
                value:
                  status: paused
              resume:
                summary: Resume a agent
                value:
                  status: active
              recompile:
                summary: Recompile a condition agent's text
                value:
                  request: notify me when ETH is above 6000 instead
      responses:
        '200':
          description: >-
            The updated agent, or a clarification if the new `request` text was
            ambiguous.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/AgentGetResponse'
                  - $ref: '#/components/schemas/NeedsClarificationResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '422':
          $ref: '#/components/responses/Unwatchable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
    delete:
      operationId: deleteAgent
      tags:
        - Agents
      summary: Delete a agent
      description: >
        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.
      responses:
        '204':
          description: Deleted, or already gone — idempotent either way.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /agents/{id}/test:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        example: w_condition_k17f2a9c
    post:
      operationId: testFireAgent
      tags:
        - Agents
      summary: Manually trigger a agent once, for integration testing
      description: >
        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).
      responses:
        '202':
          description: The test trigger was queued.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentTestFireResponse'
              example:
                value:
                  ok: true
                  note: >-
                    test event will be delivered like a real trigger, flagged
                    test:true
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '404':
          $ref: '#/components/responses/AgentNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /agent-events:
    get:
      operationId: listAgentEvents
      tags:
        - Agents
      summary: List/replay trigger events — the pull rail
      description: >
        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.
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
          description: >-
            Opaque pagination cursor from a previous response's `next_cursor`
            (the exclusive-before `triggered_at` of the last item on that page).
            A garbage cursor is treated as absent (first page), not a `400`.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: A page of trigger events.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentEventListResponse'
              examples:
                events:
                  value:
                    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
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /webhook-endpoints:
    post:
      operationId: createWebhookEndpoint
      tags:
        - Webhook delivery
      summary: Register a webhook endpoint
      description: >
        `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.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointCreateRequest'
            examples:
              register:
                value:
                  url: https://example.com/webhooks/flip
      responses:
        '201':
          description: The registered endpoint and its signing secret (shown once).
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointCreateResponse'
              examples:
                created:
                  value:
                    endpoint:
                      id: k9c7f2ab
                      url: https://example.com/webhooks/flip
                    signing_secret: >-
                      whs_3f9a7b2e1c4d8f6a0b5e9c2d7a1f4b8e6c0d3a9f7b2e5c1d8a4f0b6e3c9d7a2f
        '400':
          description: Malformed body, or a URL that fails the safety check.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidRequest:
                  value:
                    error: invalid_request
                    friendly_message: '`url` is required.'
                    issues:
                      - field: url
                        message: required string
                invalidUrl:
                  value:
                    error: invalid_url
                    friendly_message: private_ip
                    issues:
                      - field: url
                        message: private_ip
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          description: >-
            Either the per-key rate limit or the 5-endpoint-per-tenant cap was
            exceeded.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rateLimited:
                  value:
                    error: rate_limited
                    friendly_message: Rate limit exceeded for this key. Slow down and retry.
                    issues: []
                quotaExceeded:
                  value:
                    error: endpoint_quota_exceeded
                    friendly_message: >-
                      This tenant already has 5 webhook endpoints registered —
                      the maximum. Remove one before registering another.
                    issues: []
        '503':
          $ref: '#/components/responses/NotConfigured'
    get:
      operationId: listWebhookEndpoints
      tags:
        - Webhook delivery
      summary: List registered webhook endpoints
      description: >-
        Never includes secrets — only the `POST` response ever carries the raw
        `signing_secret`.
      responses:
        '200':
          description: The tenant's registered endpoints.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointListResponse'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /webhook-endpoints/{id}:
    delete:
      operationId: deleteWebhookEndpoint
      tags:
        - Webhook delivery
      summary: Remove a webhook endpoint
      description: >
        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.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: k9c7f2ab
      responses:
        '204':
          description: Removed, or already gone — idempotent either way.
          headers:
            Flip-Schema-Version:
              $ref: '#/components/headers/FlipSchemaVersion'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
webhooks:
  agentFired:
    post:
      operationId: agentFiredWebhook
      tags:
        - Webhook delivery
      summary: A agent trigger pushed to your registered endpoint
      description: >
        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:

        - The timestamp here is `created` (an ISO-8601 string); the pull rail's
          equivalent is `created_at` (an integer epoch-ms) — same instant, different
          field name and format.
        - Both rails emit `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.
      parameters:
        - name: X-Flip-Event-Id
          in: header
          required: true
          schema:
            type: string
          description: >-
            Identical to the payload's `id`. The stable dedup key across retries
            and across both delivery rails.
          example: evt_k7m2ab91
        - name: X-Flip-Signature
          in: header
          required: true
          schema:
            type: string
          description: >
            `t=<unix_seconds>,v1=<hex HMAC-SHA256(signing_secret,
            "<t>.<raw_body>")>` —

            the Stripe signed-webhooks convention. Verify with a constant-time

            comparison against the raw (unparsed) request body, and reject
            timestamps

            outside a ~5 minute tolerance. Node reference implementation:


            ```js

            const crypto = require("crypto");


            function verifyFlipSignature(secret, header, rawBody, toleranceSec =
            300) {
              const parts = Object.fromEntries(
                header.split(",").map((kv) => {
                  const i = kv.indexOf("=");
                  return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
                }),
              );
              const { t, v1 } = parts;
              if (!t || !v1) return false;
              if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;

              const expected = crypto
                .createHmac("sha256", secret)
                .update(`${t}.${rawBody}`)
                .digest("hex");

              const expectedBuf = Buffer.from(expected, "hex");
              const gotBuf = Buffer.from(v1, "hex");
              if (expectedBuf.length !== gotBuf.length) return false;
              return crypto.timingSafeEqual(expectedBuf, gotBuf);
            }

            ```
          example: >-
            t=1753142400,v1=5f6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEventPayload'
            examples:
              triggered:
                value:
                  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
              test:
                value:
                  id: evt_k7m2ac02
                  type: agent.test
                  created: '2026-07-30T18:41:10.000Z'
                  agent:
                    id: k9d81ab2
                    kind: price
                  trigger:
                    headline: ⚠️ ETH price above $5,000
                    kind: price
                    condition: above
                    threshold: 5000
                    wallet: 0x8f2a...d5
      responses:
        '200':
          description: >-
            Any `2xx` within 10 seconds acknowledges the delivery; the response
            body is ignored.
components:
  securitySchemes:
    PartnerKey:
      type: http
      scheme: bearer
      bearerFormat: fk_...
      description: >
        A partner API key, presented as `Authorization: Bearer fk_...`. Keys are

        scoped to a partner tenant with a rate-limit tier and authorize metered

        spend — keep them server-side. Request early-access keys via
        fliplabs.ai.
  headers:
    FlipSchemaVersion:
      description: The public contract version this response was built against.
      schema:
        type: string
        example: '2026-07-15'
  parameters:
    AcceptStream:
      name: Accept
      in: header
      required: false
      description: >-
        Set to `text/event-stream` to receive the plan as Server-Sent Events.
        Any other value (or omitted) yields a single buffered JSON response.
      schema:
        type: string
        enum:
          - application/json
          - text/event-stream
        default: application/json
  responses:
    InternalError:
      description: >-
        Something failed on Flip's side. The request was well-formed; retrying
        is reasonable, and a persistent `internal_error` is worth reporting.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: internal_error
            friendly_message: Couldn't complete that right now — try again shortly.
            issues: []
    InvalidPatch:
      description: >-
        The patch is well-formed but not applicable to this agent. Today that
        means a `request` edit on a typed agent: only plain-English condition
        agents can be recompiled from a new request — delete and recreate a
        typed one instead.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: invalid_patch
            friendly_message: >-
              `request` edits are only supported for plain-English condition
              agents.
            issues: []
    InvalidRequest:
      description: >-
        Malformed body, or an unsupported chain/asset. `issues[]` lists the
        offending fields.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: invalid_request
            friendly_message: '`prompt` is required.'
            issues:
              - field: prompt
                message: required string
    InvalidKey:
      description: Missing, malformed, revoked, or invalid API key.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: invalid_key
            friendly_message: >-
              Missing or malformed API key. Pass it as `Authorization: Bearer
              fk_...`.
            issues: []
    InsufficientCredits:
      description: The tenant's credit balance is below the call cost.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: insufficient_credits
            friendly_message: >-
              This call costs 5 credits; the tenant balance is 3. Top up to
              continue.
            issues: []
    ScopeForbidden:
      description: The key is not authorized for the requested scope.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: scope_forbidden
            friendly_message: This key is not authorized for "intent".
            issues: []
    RouteExpired:
      description: >-
        A pinned route is no longer available; re-plan the affected step via
        `/plan`.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: route_expired
            friendly_message: This route just expired. Re-plan the step and try again.
            issues: []
    RateLimited:
      description: Per-key rate-limit tier exceeded. Back off and retry.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: rate_limited
            friendly_message: Rate limit exceeded for this key. Slow down and retry.
            issues: []
    EngineError:
      description: The upstream planning/simulation engine was unreachable or errored.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: engine_error
            friendly_message: The planning engine returned an error.
            issues: []
    NotConfigured:
      description: >-
        The API is temporarily unavailable. `not_configured` means the service
        is not set up; `compile_unavailable` means the plain-English compiler is
        transiently down and the call is worth retrying in a few seconds — typed
        agent creation is unaffected either way.
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            notConfigured:
              value:
                error: not_configured
                friendly_message: The API is not available right now.
                issues: []
            compileUnavailable:
              value:
                error: compile_unavailable
                friendly_message: >-
                  Couldn't compile that request right now — this is transient,
                  retry in a few seconds. Typed agent creation is unaffected.
                issues: []
    Unwatchable:
      description: >-
        Not a watchable condition, or the compiled check was refused by an
        internal match-gate (it doesn't faithfully match what was asked).
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: unwatchable
            friendly_message: >-
              That isn't a watchable condition — try a measurable trigger like
              "notify me when ETH is above 5000".
            issues: []
    AgentNotFound:
      description: >-
        No agent with that id (a structurally-invalid id, or a real id this
        tenant doesn't own).
      headers:
        Flip-Schema-Version:
          $ref: '#/components/headers/FlipSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: agent_not_found
            friendly_message: No agent with that id.
            issues: []
  schemas:
    Error:
      type: object
      description: The single error envelope every 4xx/5xx response uses.
      required:
        - error
        - friendly_message
        - issues
      properties:
        error:
          type: string
          description: A stable machine-readable error code.
          examples:
            - invalid_request
            - invalid_key
            - insufficient_credits
            - route_expired
            - rate_limited
        friendly_message:
          type: string
          description: A human-readable, user-safe explanation.
        issues:
          type: array
          description: Structured field-level problems, where relevant. Often empty.
          items:
            $ref: '#/components/schemas/Issue'
      additionalProperties: false
    Issue:
      type: object
      required:
        - message
      properties:
        field:
          type: string
          description: The offending request field, if the problem is field-specific.
        message:
          type: string
          description: What is wrong with that field.
      additionalProperties: false
    Wallet:
      type: object
      description: >-
        A wallet in play. The planner prefers these for sourcing and
        destinations.
      required:
        - address
        - chain
      properties:
        address:
          type: string
          description: EVM 0x address or Solana base58 address (never lower-cased).
          example: 0x8f2a...d5
        chain:
          type: string
          description: Chain key, e.g. ethereum, base, arbitrum, optimism, polygon, solana.
          example: ethereum
        label:
          type: string
          description: Optional human label surfaced in reasoning.
        balances:
          type: array
          description: >-
            Optional on-chain holdings for this wallet, read by the feasibility
            gate in `/plan`.
          items:
            $ref: '#/components/schemas/TokenBalance'
      additionalProperties: true
    TokenBalance:
      type: object
      required:
        - symbol
        - amount
      properties:
        symbol:
          type: string
          example: USDC
        amount:
          type: number
          description: Token-denominated amount (not wei).
          example: 12400
        usd_value:
          type: number
        chain:
          type: string
          description: >-
            Per-token chain, so summed-across-chains balances aren't
            mis-attributed.
      additionalProperties: true
    Balance:
      type: object
      description: Pre-fetched holdings for a wallet so the planner sizes without a lookup.
      required:
        - address
        - chain
      properties:
        address:
          type: string
        chain:
          type: string
        eth_balance:
          type: number
        usdc_balance:
          type: number
        stable_usd:
          type: number
        total_usd:
          type: number
        tokens:
          type: array
          items:
            $ref: '#/components/schemas/TokenBalance'
      additionalProperties: true
    IntentRequest:
      type: object
      required:
        - prompt
        - wallets
      properties:
        prompt:
          type: string
          minLength: 1
          maxLength: 2000
          description: The desired outcome in plain English.
          example: move my idle USDC into the best stable yield across my chains
        wallets:
          type: array
          minItems: 1
          description: The wallets in play — at least one.
          items:
            $ref: '#/components/schemas/Wallet'
        balances:
          type: array
          description: Optional pre-fetched holdings. Omit and Flip fetches them.
          items:
            $ref: '#/components/schemas/Balance'
        conversation_context:
          type: string
          maxLength: 6000
          description: >
            Prior turns of the conversation, for multi-turn planning: the prior
            turns

            newline-joined, one turn per line, each line `User: <text>` or

            `Flip: <text>`, most recent last, ~8 turns max. The current message
            goes

            in `prompt`, never in this block. Truncated at 6000 characters.
          example: >-
            User: move my idle USDC into the best stable yield

            Flip: 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.
        clarify_follow_up:
          type: boolean
          description: >
            Set `true` when re-sending after the user picked a clarification
            option.

            The re-send pattern: `prompt` = the original prompt + `"\n\n"` + the

            plain-text answer block `Clarification — <question>: <picked
            label>`.

            The flag makes the planner acknowledge the answer and continue
            planning —

            it never re-raises the same clarification.
        options:
          type: object
          description: Coarse run options. Raw model ids are never exposed.
          properties:
            stream:
              type: boolean
              description: >-
                Stream the build as SSE (equivalent to `Accept:
                text/event-stream`).
            tier:
              type: string
              enum:
                - fast
                - quality
              description: Coarse latency/quality preference.
          additionalProperties: false
      additionalProperties: false
    Intent:
      type: object
      description: >
        One planned action — *what* to do, not yet *how*. `action` selects which
        of the

        other fields apply. `amount` is a token-denominated number, or the
        string `max`

        (use the full available balance) or `chained` (size from the previous
        step's real

        output at sign time). Common per-action fields:

        transfer/stake/unstake/deposit/withdraw/borrow/repay use `asset`,
        `chain`, `from`,

        `amount`; `bridge` uses `from_chain`/`to_chain` (+ optional `to_asset`);
        `swap`

        uses `from_asset`/`to_asset` (+ optional `to_chain`);
        `deposit`/`withdraw` add

        `protocol`, `pool_id`, `vault_address`; `sweep` uses `sources[]` +
        `destination`;

        `lp-add` uses `token0`/`token1`, `fee_tier` (Uniswap units, e.g. 3000 =
        0.3%),

        `amount0`/`amount1`, optional `slippage_bps` — full-range positions
        only;

        `lp-remove`/`lp-collect` reference the position by pair + chain.
      required:
        - action
      properties:
        action:
          type: string
          enum:
            - transfer
            - sweep
            - bridge
            - deposit
            - withdraw
            - stake
            - unstake
            - claim
            - approve
            - swap
            - borrow
            - repay
            - lp-add
            - lp-remove
            - lp-collect
          description: The kind of on-chain action.
        asset:
          type: string
          description: The token symbol acted on.
          example: USDC
        chain:
          type: string
          description: The chain the action happens on.
          example: base
        from_chain:
          type: string
          description: Source chain (bridge; or cross-chain swap).
        to_chain:
          type: string
          description: Destination chain (bridge; or cross-chain swap).
        from_asset:
          type: string
          description: Source token (swap).
        to_asset:
          type: string
          description: >-
            Destination token (swap; or destination-side conversion on a
            bridge).
        from:
          type: string
          description: The source wallet address.
        to:
          type: string
          description: The destination wallet address (transfer).
        spender:
          type: string
          description: The contract being granted allowance (approve).
        protocol:
          type: string
          description: >-
            Protocol key for deposit/withdraw/claim/borrow/repay, e.g. aave,
            morpho.
        pool_id:
          type: string
          description: Catalog pool id for the target market.
        vault_address:
          type: string
          description: The vault/market contract address.
        amount:
          $ref: '#/components/schemas/Amount'
        min_amount:
          type: number
          description: Floor amount for a sweep source to be included.
        slippage_bps:
          type: integer
          minimum: 1
          maximum: 5000
          description: Slippage tolerance in basis points (swap/bridge).
        sources:
          type: array
          description: Sweep sources — each `{ chain, wallet }`.
          items:
            type: object
            properties:
              chain:
                type: string
              wallet:
                type: string
        destination:
          type: object
          description: Sweep destination — `{ chain, wallet }`.
          properties:
            chain:
              type: string
            wallet:
              type: string
      additionalProperties: true
    Amount:
      description: A token-denominated number, or the literal `max` or `chained`.
      oneOf:
        - type: number
        - type: string
          enum:
            - max
            - chained
      example: max
    Clarification:
      type: object
      description: A blocking question the caller must resolve before a plan can be built.
      required:
        - question
        - options
      properties:
        id:
          type: string
          description: Stable identifier for the clarification.
        kind:
          type: string
          enum:
            - chain
            - asset
            - wallet
            - amount
            - other
          description: Drives a typed picker in the UI.
        question:
          type: string
          description: The question to put to the user.
        options:
          type: array
          items:
            $ref: '#/components/schemas/ClarificationOption'
      additionalProperties: true
    ClarificationOption:
      type: object
      required:
        - id
        - label
      properties:
        id:
          type: string
        label:
          type: string
        value:
          type: string
          description: Optional machine value to fold back into the re-POSTed prompt.
      additionalProperties: true
    IntentResponse:
      type: object
      description: >-
        The buffered result of `/intent`. Either `intents` or `clarifications`
        is populated.
      required:
        - reasoning
        - intents
      properties:
        reasoning:
          type: string
          description: Plain-prose explanation of the plan, written for the end user.
        suggested_name:
          type: string
          description: >-
            A short template name for the workflow. Empty for informational
            answers.
        kind:
          type: string
          enum:
            - transactional
            - informational
          description: Whether the prompt produced a transaction plan or a plain answer.
        intents:
          type: array
          description: >-
            The structured actions to resolve via `/plan`. Empty when
            clarifications are raised.
          items:
            $ref: '#/components/schemas/Intent'
        clarifications:
          type: array
          description: >-
            Blocking questions when the request is ambiguous. Empty when
            `intents` is populated.
          items:
            $ref: '#/components/schemas/Clarification'
        dropped:
          type: array
          description: Intents the engine could not fulfill, with a reason. Informational.
          items:
            type: object
            additionalProperties: true
      additionalProperties: true
    IntentStreamEvent:
      type: object
      description: >
        One Server-Sent Event on the `/intent` stream. The wire format is

        `event: <name>\ndata: <json>\n\n`. Event names: `delta` and `reasoning`

        (`{ text }` prose fragments), `tool-start` / `tool-end` (`{ name }`),
        and a

        terminal `final` carrying the full `IntentResponse`.
      properties:
        event:
          type: string
          enum:
            - delta
            - reasoning
            - tool-start
            - tool-end
            - final
        data:
          description: The JSON payload for the event (shape depends on `event`).
          oneOf:
            - type: object
              properties:
                text:
                  type: string
            - type: object
              properties:
                name:
                  type: string
            - $ref: '#/components/schemas/IntentResponse'
      additionalProperties: true
    PlanRequest:
      type: object
      required:
        - intents
        - wallets
      properties:
        intents:
          type: array
          minItems: 1
          description: The `intents[]` from `/intent` (or hand-built to the same shape).
          items:
            $ref: '#/components/schemas/Intent'
        wallets:
          type: array
          minItems: 1
          description: >-
            Signing wallets, each ideally carrying the on-chain `balances` the
            feasibility gate reads.
          items:
            $ref: '#/components/schemas/Wallet'
      additionalProperties: false
    Step:
      type: object
      description: >
        One unsigned transaction. EVM steps carry `to`/`data`/`value` calldata;
        Solana

        steps carry a base64 `solana_tx` (+ `solana_format`) instead. A step
        whose

        `amount` is `chained` is sized from the previous step's real output at
        sign time

        — re-encode it via `/steps/reencode` before signing.
      required:
        - order
        - type
        - chain
        - to
        - data
        - value
      properties:
        order:
          type: integer
          description: 1-based execution order.
        type:
          type: string
          enum:
            - transfer
            - bridge
            - approve
            - deposit
            - withdraw
            - stake
            - unstake
            - claim
            - swap
            - borrow
            - repay
            - wrap
        chain:
          type: string
        chain_id:
          type: integer
        protocol:
          type: string
        action_type:
          type: string
          description: Human-facing action label, e.g. "Bridge", "Deposit".
        asset:
          type: string
        from_chain:
          type: string
        to_chain:
          type: string
        amount:
          type: number
          description: Token-denominated amount. 0 for a chained step until re-encoded.
        amount_raw:
          type: string
          description: Base-unit (wei / smallest-unit) amount as a decimal string.
        to:
          type: string
          description: EVM target contract address. `0x` on Solana steps.
        data:
          type: string
          description: EVM calldata. `0x` on Solana steps.
        value:
          type: string
          description: Native value in wei as a decimal string.
        description:
          type: string
        estimated_gas:
          type: string
        bridge_fee:
          type: number
        bridge_time:
          type: string
        wait_for_bridge:
          type: boolean
          description: True when this step must wait for a preceding bridge mint to land.
        chained:
          type: object
          description: Present when the amount was chained from an earlier producer step.
          properties:
            from_key:
              type: string
              description: The `${wallet}@${chain}@${asset}` of the producer output.
            estimate_amount:
              type: number
            min_output:
              type: number
          additionalProperties: true
        pool_id:
          type: string
        solana_tx:
          type: string
          description: Base64 Solana transaction. Only set when `chain` is solana.
        solana_format:
          type: string
          enum:
            - legacy
            - v0
        solana_error:
          type: string
        slippage:
          type: number
        fee_bps:
          type: number
        swap_tool:
          type: string
          description: Pinned aggregator tool key (e.g. sushiswap-v3) for correct re-quote.
        error:
          type: string
          description: Set when the planner couldn't build this step.
        success:
          type: boolean
      additionalProperties: true
    PlanResponse:
      type: object
      required:
        - steps
      properties:
        steps:
          type: array
          items:
            $ref: '#/components/schemas/Step'
        summary:
          $ref: '#/components/schemas/PlanSummary'
      additionalProperties: true
    PlanSummary:
      type: object
      properties:
        total_after_fees:
          type: string
          example: 12,391.4 USDC
        bridge_count:
          type: integer
        est_fee_usd:
          type: string
          example: '3.10'
      additionalProperties: true
    ReencodeRequest:
      type: object
      required:
        - step
        - actual_amount
      properties:
        step:
          $ref: '#/components/schemas/Step'
        actual_amount:
          type: number
          description: >-
            The real amount, in token units (not wei), to re-encode the step
            for.
          example: 12391.4
      additionalProperties: false
    ReencodeResponse:
      type: object
      required:
        - ok
      properties:
        ok:
          type: boolean
        data:
          type: string
          description: Freshly encoded EVM calldata.
        amount_raw:
          type: string
        value:
          type: string
        solana_tx:
          type: string
          description: Re-built base64 Solana transaction (Solana steps).
        solana_format:
          type: string
          enum:
            - legacy
            - v0
        unsupported_reason:
          type: string
          description: >-
            Set with `ok: false` when the step can't be re-encoded. HARD-FAIL:
            never fall back to signing plan-time calldata — it embeds quote-time
            amounts and deadlines. Show this reason in the UI and stop (re-plan
            to continue).
        quoted_at:
          type: string
          format: date-time
          description: >-
            When this quote was priced. Quotes are perishable — the embedded
            minimum output is set at quote time, so open the wallet immediately
            and call reencode again if the user idles (~30s+).
      additionalProperties: true
    SimulateRequest:
      type: object
      required:
        - chain
        - sender
        - steps
      properties:
        chain:
          type: string
          description: >-
            The chain the steps execute on — one simulate call per chain. EVM
            slugs dispatch to the Tenderly fork; `solana` dispatches to the
            Solana simulator.
          enum:
            - ethereum
            - base
            - arbitrum
            - optimism
            - polygon
            - solana
          example: arbitrum
        sender:
          type: string
          description: >-
            The address the simulation runs FROM (your user's signing wallet).
            EVM: required 0x-hex. Solana: applied to any step missing its own
            `wallet`.
          example: '0x0036534C48a754163F0f446601f36BCfdA840f57'
        steps:
          type: array
          minItems: 1
          description: The `steps[]` from `/plan` to dry-run.
          items:
            $ref: '#/components/schemas/Step'
      additionalProperties: false
    SimulateResponse:
      type: object
      required:
        - success
        - steps
      description: >-
        The LIVE response contract as returned by the simulation engine
        (documented from real responses, 2026-07-15 — earlier spec versions
        published a smaller shape that never matched the wire).
      properties:
        success:
          type: boolean
          description: True when every step simulated successfully.
        gas_used:
          type: string
          description: Total gas across steps, comma-formatted (e.g. "608,282").
          example: 608,282
        gas_price:
          type: string
          description: Gas price used for the $ estimate (e.g. "0.02 gwei").
          example: 0.02 gwei
        gas_price_source:
          type: string
          description: >-
            "rpc" when the price came from a live per-chain RPC read; "fallback"
            when the hardcoded last-resort price was used ($ figures are
            approximate in that case).
          enum:
            - rpc
            - fallback
        total_cost:
          type: string
          description: Estimated total fee in USD (e.g. "$0.02").
          example: $0.02
        error:
          type: string
          nullable: true
          description: >-
            Human-readable revert reason when the batch failed (e.g. "Return
            amount is not enough", "ERC20: transfer amount exceeds balance") —
            safe to show directly in a UI.
        steps:
          type: array
          items:
            $ref: '#/components/schemas/SimulateStepResult'
      additionalProperties: true
    SimulateStepResult:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
        status:
          type: string
          description: Emoji status marker ("✅" / "❌") — display convenience only.
        status_code:
          type: integer
        action:
          type: string
          description: Human label for the step (e.g. "Approve USDC for Kyberswap").
        action_type:
          type: string
          description: Step category (e.g. "Approve", "Transfer", "Swap").
        protocol:
          type: string
        asset:
          type: string
        gas:
          type: string
          description: Per-step gas, comma-formatted string.
        error:
          type: string
          nullable: true
          description: >-
            Human-readable revert reason for THIS step — show it in the chat
            card before opening the wallet.
        asset_changes:
          type: array
          description: Balance changes observed in the fork (may be empty).
          items:
            type: object
            additionalProperties: true
      additionalProperties: true
    UsageResponse:
      type: object
      required:
        - balance
        - cost_per_call
        - recent
      properties:
        balance:
          type: integer
          description: Current per-tenant credit balance.
        cost_per_call:
          type: integer
          description: Flat credit cost per metered call.
          example: 5
        recent:
          type: array
          items:
            $ref: '#/components/schemas/UsageEntry'
      additionalProperties: false
    UsageEntry:
      type: object
      properties:
        kind:
          type: string
          enum:
            - spend
            - grant
            - refund
        amount:
          type: integer
        reason:
          type: string
          example: api_intent
        at:
          type: integer
          description: Unix epoch milliseconds.
      additionalProperties: true
    MetaResponse:
      type: object
      required:
        - schema_version
        - cost_per_call
        - actions
        - chains
      properties:
        schema_version:
          type: string
          example: '2026-07-15'
        cost_per_call:
          type: integer
          example: 5
        actions:
          type: array
          description: The supported intent actions.
          items:
            type: string
        chains:
          type: array
          description: The supported chains.
          items:
            type: string
        rate_tiers:
          type: object
          description: The per-key rate-limit tiers.
          additionalProperties:
            type: object
            properties:
              max_requests:
                type: integer
              window_ms:
                type: integer
      additionalProperties: true
    AgentWallets:
      type: array
      minItems: 1
      maxItems: 10
      description: >-
        EVM `0x` + 40 hex, or Solana base58 (32-44 chars). Case is preserved on
        the way in/out for every kind, including `health` (Solana addresses are
        case-sensitive).
      items:
        type: string
      example:
        - 0x8f2a...d5
    PlainEnglishAgentCreate:
      type: object
      required:
        - request
        - wallets
      properties:
        request:
          type: string
          minLength: 3
          maxLength: 500
          example: notify me when ETH is above 5000
        wallets:
          $ref: '#/components/schemas/AgentWallets'
      additionalProperties: false
    PriceAgentCreate:
      type: object
      description: Typed create — price.
      required:
        - kind
        - wallets
        - asset_symbol
        - coingecko_id
        - condition
        - threshold
      properties:
        kind:
          type: string
          enum:
            - price
        wallets:
          $ref: '#/components/schemas/AgentWallets'
        asset_symbol:
          type: string
          maxLength: 20
          example: ETH
        coingecko_id:
          type: string
          maxLength: 100
          description: >-
            The literal CoinGecko id (e.g. `ethereum`) — not resolved from
            `asset_symbol` server-side.
          example: ethereum
        condition:
          type: string
          enum:
            - above
            - below
        threshold:
          type: number
          exclusiveMinimum: 0
          example: 5000
        pct_change:
          type: number
          exclusiveMinimum: 0
          description: >-
            Optional — trigger on a percent move instead of an absolute
            threshold cross.
        base_price:
          type: number
          exclusiveMinimum: 0
          description: Reference price `pct_change` is measured from, when set.
      additionalProperties: false
    YieldAgentCreate:
      type: object
      description: Typed create — yield.
      required:
        - kind
        - wallets
        - pool_id
        - protocol
        - chain
        - asset
        - condition
        - threshold
      properties:
        kind:
          type: string
          enum:
            - yield
        wallets:
          $ref: '#/components/schemas/AgentWallets'
        pool_id:
          type: string
          maxLength: 200
          description: Catalog pool id for the target market.
        protocol:
          type: string
          maxLength: 60
        chain:
          type: string
          maxLength: 40
        asset:
          type: string
          maxLength: 20
        metric:
          type: string
          enum:
            - apy
            - supplyApy
            - borrowApy
          default: apy
        condition:
          type: string
          enum:
            - above
            - below
        threshold:
          type: number
          minimum: 0
      additionalProperties: false
    HealthAgentCreate:
      type: object
      description: >-
        Typed create — Aave/Spark health factor. `threshold` is a decimal health
        factor (e.g. `1.5`), not a percent — values `>= 100` are rejected as a
        likely mistake.
      required:
        - kind
        - wallets
        - protocol
        - chain
        - threshold
      properties:
        kind:
          type: string
          enum:
            - health
        wallets:
          $ref: '#/components/schemas/AgentWallets'
        target_wallet:
          type: string
          description: The address to watch, if different from `wallets[0]` (the owner).
        protocol:
          type: string
          enum:
            - aave
            - spark
        chain:
          type: string
          maxLength: 40
        threshold:
          type: number
          exclusiveMinimum: 0
          exclusiveMaximum: 100
          example: 1.5
      additionalProperties: false
    PortfolioAgentCreate:
      type: object
      description: >-
        Typed create — portfolio move. Always triggers on an "above" move of
        `threshold` percent; there is no "below" direction for this kind.
      required:
        - kind
        - wallets
        - threshold
      properties:
        kind:
          type: string
          enum:
            - portfolio
        wallets:
          $ref: '#/components/schemas/AgentWallets'
        threshold:
          type: number
          exclusiveMinimum: 0
          description: Percent move, absolute.
        asset_symbol:
          type: string
          maxLength: 20
          description: Specific symbol to watch, or omit for the whole portfolio.
        movement_window:
          type: string
          enum:
            - 24h
            - 7d
          default: 24h
      additionalProperties: false
    ScheduleAgentCreate:
      type: object
      description: >-
        Typed create — a recurring notify-only check-in. Unlike the
        plain-English schedule path (which anchors to UTC noon), this variant
        takes an explicit `timezone`.
      required:
        - kind
        - wallets
        - cadence
        - hour
        - minute
        - timezone
      properties:
        kind:
          type: string
          enum:
            - schedule
        wallets:
          $ref: '#/components/schemas/AgentWallets'
        cadence:
          type: string
          enum:
            - daily
            - weekly
            - monthly
        hour:
          type: integer
          minimum: 0
          maximum: 23
          description: Local hour, per `timezone`.
        minute:
          type: integer
          minimum: 0
          maximum: 59
        day_of_week:
          type: integer
          minimum: 0
          maximum: 6
          description: 'Required for `cadence: weekly` (0 = Sunday … 6 = Saturday).'
        day_of_month:
          type: integer
          minimum: 1
          maximum: 31
          description: 'Required for `cadence: monthly` (clamped to the month''s length).'
        timezone:
          type: string
          description: IANA timezone, e.g. `America/New_York`.
        label:
          type: string
          description: Optional display name. Defaults to "Portfolio check-in".
      additionalProperties: false
    AgentCreateRequest:
      description: >-
        Either the plain-English shape, or one of the five typed-kind shapes
        (discriminated by `kind`). Never both, never neither.
      oneOf:
        - $ref: '#/components/schemas/PlainEnglishAgentCreate'
        - $ref: '#/components/schemas/PriceAgentCreate'
        - $ref: '#/components/schemas/YieldAgentCreate'
        - $ref: '#/components/schemas/HealthAgentCreate'
        - $ref: '#/components/schemas/PortfolioAgentCreate'
        - $ref: '#/components/schemas/ScheduleAgentCreate'
    Agent:
      type: object
      description: >
        The normalized shape for every agent kind — fields present depend on
        `kind`;

        absent fields are simply omitted (this is one flexible shape, not a
        oneOf,

        matching how `Step` is documented elsewhere in this spec).


        `status` is always one of exactly three public values, regardless of
        which

        internal table a agent lives in: `active`, `paused` (either one settable
        via

        `PATCH .../{id}`), or `degraded` — `condition` agents only, set by the
        cron

        when the compiled check keeps returning "unknown" (a read-only state,
        never

        partner-set; resolves back to `active` once the check succeeds again). A

        `price`/`yield`/`health`/`portfolio` agent that already triggered once
        and

        re-armed still shows `active`, never a separate "triggered" state.
      required:
        - id
        - type
        - kind
        - status
        - wallets
      properties:
        id:
          type: string
          description: >-
            `w_<type>_<internal id>` — use this verbatim in `/agents/{id}`
            calls.
          example: w_condition_k17f2a9c
        type:
          type: string
          enum:
            - alert
            - condition
            - reminder
          description: >-
            The underlying storage table. Redundant with the id's own prefix;
            exposed for convenience.
        kind:
          type: string
          enum:
            - price
            - yield
            - health
            - portfolio
            - news
            - condition
            - schedule
          description: >-
            The public kind vocabulary. `news` can appear on a listing but is
            not a creatable kind through this API.
        status:
          type: string
          enum:
            - active
            - paused
            - degraded
          description: See the schema description above.
        created_at:
          type: integer
          description: Unix epoch milliseconds.
        last_checked_at:
          type: integer
          description: Unix epoch milliseconds. Alert/condition kinds only.
        last_fired_at:
          type: integer
          description: Unix epoch milliseconds.
        wallets:
          $ref: '#/components/schemas/AgentWallets'
        condition:
          type: string
          enum:
            - above
            - below
          description: >-
            price/yield/health/portfolio kinds. Always `above` for portfolio;
            always `below` for health.
        threshold:
          type: number
        asset_symbol:
          type: string
          description: price/portfolio kinds.
        coingecko_id:
          type: string
          description: price kind.
        pct_change:
          type: number
          description: price kind, when set at create.
        base_price:
          type: number
          description: price kind, when set at create.
        pool_id:
          type: string
          description: yield kind.
        protocol:
          type: string
          description: yield/health kinds.
        chain:
          type: string
          description: yield/health kinds.
        asset:
          type: string
          description: yield kind.
        metric:
          type: string
          enum:
            - apy
            - supplyApy
            - borrowApy
          description: yield kind.
        target_wallet:
          type: string
          description: health kind, when it differs from the owner wallet.
        movement_window:
          type: string
          enum:
            - 24h
            - 7d
          description: portfolio kind.
        rendered:
          type: string
          description: >-
            condition kind — the plan's plain-English readback (what actually
            triggers).
        fire_headline:
          type: string
          description: condition kind — custom one-line trigger headline, when set.
        cadence:
          type: string
          enum:
            - hourly
            - daily
            - weekly
            - monthly
          description: >-
            condition kind (hourly/daily) or schedule kind
            (daily/weekly/monthly).
        intent:
          type: string
          description: condition kind — the original plain-English text.
        label:
          type: string
          description: schedule kind — display name.
        hour:
          type: integer
          description: schedule kind.
        minute:
          type: integer
          description: schedule kind.
        day_of_week:
          type: integer
          description: schedule kind, cadence weekly.
        day_of_month:
          type: integer
          description: schedule kind, cadence monthly.
        timezone:
          type: string
          description: schedule kind.
      additionalProperties: true
    AgentCreateResponse:
      type: object
      required:
        - agent
      properties:
        agent:
          $ref: '#/components/schemas/Agent'
        compile:
          type: object
          nullable: true
          description: >-
            Present (non-null) only when created from a plain-English condition
            request.
          properties:
            rendered:
              type: string
            fire_headline:
              type: string
              nullable: true
            cadence:
              type: string
              enum:
                - hourly
                - daily
          additionalProperties: true
      additionalProperties: true
    AgentGetResponse:
      type: object
      required:
        - agent
      properties:
        agent:
          $ref: '#/components/schemas/Agent'
      additionalProperties: false
    AgentListResponse:
      type: object
      required:
        - agents
        - next_cursor
      properties:
        agents:
          type: array
          items:
            $ref: '#/components/schemas/Agent'
        next_cursor:
          type: string
          nullable: true
      additionalProperties: false
    AgentPatchRequest:
      oneOf:
        - type: object
          required:
            - status
          properties:
            status:
              type: string
              enum:
                - paused
                - active
          additionalProperties: false
        - type: object
          required:
            - request
          properties:
            request:
              type: string
              minLength: 3
              maxLength: 500
          additionalProperties: false
    NeedsClarificationResponse:
      type: object
      required:
        - status
        - question
        - options
      properties:
        status:
          type: string
          enum:
            - needs_clarification
        question:
          type: string
        options:
          type: array
          maxItems: 4
          items:
            $ref: '#/components/schemas/ClarificationOption'
      additionalProperties: false
    AgentTestFireResponse:
      type: object
      required:
        - ok
      properties:
        ok:
          type: boolean
        note:
          type: string
      additionalProperties: false
    AgentEvent:
      type: object
      description: >-
        One item from the `GET /agent-events` pull rail. Same event mapping as
        the pushed webhook body for the same trigger (see `WebhookEventPayload`)
        — identical `id`/`type`/`agent`/`trigger` field set, except this rail's
        timestamp is `created_at` (epoch-ms) instead of `created` (ISO-8601),
        and this rail's `trigger` object is `snake_case` while the pushed
        payload's currently isn't (see `WebhookEventPayload.trigger`'s note).
      required:
        - id
        - type
        - created_at
        - agent
        - trigger
      properties:
        id:
          type: string
          description: >-
            `evt_<internal notification id>` — identical to the pushed webhook's
            `id` / `X-Flip-Event-Id` for the same trigger.
          example: evt_k7m2ab91
        type:
          type: string
          enum:
            - agent.triggered
            - agent.degraded
            - agent.test
        created_at:
          type: integer
          description: Unix epoch milliseconds.
        agent:
          type: object
          properties:
            id:
              type: string
              description: >-
                The bare internal agent row id — NOT the `w_<type>_<id>` form
                used elsewhere in this API. Identical to this same trigger's
                `agent.id` on the pushed webhook payload, with one exception: a
                manual test-trigger (`POST /agents/{id}/test`) on a
                `condition`-kind agent currently omits this back-reference on
                both rails, so it arrives empty for that one case
                (price/yield/health/portfolio/schedule test-triggers are fine).
            kind:
              type: string
          additionalProperties: true
        trigger:
          type: object
          description: >-
            `condition` and `threshold` are present on every trigger (they're
            non-optional internally) but are placeholder values (`"above"` /
            `0`) for `condition`- and `schedule`-kind triggers, where they
            aren't meaningful — trust them only for
            `price`/`yield`/`health`/`portfolio` kinds.
          properties:
            headline:
              type: string
            kind:
              type: string
            condition:
              type: string
              enum:
                - above
                - below
            threshold:
              type: number
            value_at_trigger:
              type: number
            price_at_trigger:
              type: number
            change_pct:
              type: number
            wallet:
              type: string
          additionalProperties: true
      additionalProperties: true
    AgentEventListResponse:
      type: object
      required:
        - events
        - next_cursor
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/AgentEvent'
        next_cursor:
          type: integer
          nullable: true
      additionalProperties: false
    WebhookEventPayload:
      type: object
      description: >-
        The body POSTed to your registered endpoint for each trigger. Same event
        mapping as `AgentEvent` (the `GET /agent-events` pull rail's shape) for
        the same trigger — see `POST /webhook-endpoints`'s `webhooks.agentFired`
        entry for the full delivery contract (headers, retries) and the one
        known difference in `trigger`'s key casing.
      required:
        - id
        - type
        - created
        - agent
        - trigger
      properties:
        id:
          type: string
          example: evt_k7m2ab91
        type:
          type: string
          enum:
            - agent.triggered
            - agent.degraded
            - agent.test
        created:
          type: string
          format: date-time
          description: >-
            ISO-8601. (The pull rail's equivalent field is `created_at`, an
            integer epoch-ms, for the same instant.)
        agent:
          type: object
          properties:
            id:
              type: string
              description: >-
                The public `w_<type>_<id>` agent id — the same value
                create/list/get return, and identical to this trigger's
                `agent.id` on the pull rail. Pass it back verbatim on
                `/agents/{id}` calls.
            kind:
              type: string
          additionalProperties: true
        trigger:
          type: object
          description: >-
            Same field set as `AgentEvent.trigger` (`headline`, `kind`,
            `condition`, `threshold`, and — sparse, per-kind —
            `value_at_trigger`/`price_at_trigger`/ `change_pct`, plus `wallet`)
            — `snake_case`, same as the pull rail. The pull rail's `trigger`
            object carries the identical data, identically cased.
          required:
            - headline
            - kind
            - wallet
          properties:
            headline:
              type: string
            kind:
              type: string
            condition:
              type: string
              enum:
                - above
                - below
            threshold:
              type: number
            value_at_trigger:
              type: number
            price_at_trigger:
              type: number
            change_pct:
              type: number
            wallet:
              type: string
          additionalProperties: true
      additionalProperties: true
    WebhookEndpointCreateRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          maxLength: 2048
          example: https://example.com/webhooks/flip
      additionalProperties: false
    WebhookEndpointCreateResponse:
      type: object
      required:
        - endpoint
        - signing_secret
      properties:
        endpoint:
          type: object
          required:
            - id
            - url
          properties:
            id:
              type: string
            url:
              type: string
          additionalProperties: false
        signing_secret:
          type: string
          description: >-
            Shown exactly once. Store it now — it cannot be retrieved again
            (only its hash is kept server-side).
          example: whs_3f9a7b2e1c4d8f6a0b5e9c2d7a1f4b8e6c0d3a9f7b2e5c1d8a4f0b6e3c9d7a2f
      additionalProperties: false
    WebhookEndpoint:
      type: object
      required:
        - id
        - url
        - status
      properties:
        id:
          type: string
        url:
          type: string
        status:
          type: string
          enum:
            - active
            - disabled
        disabled_reason:
          type: string
          description: >-
            Set when the delivery worker auto-disabled this endpoint (10
            consecutive dead-lettered deliveries).
        created_at:
          type: integer
          description: Unix epoch milliseconds.
      additionalProperties: false
    WebhookEndpointListResponse:
      type: object
      required:
        - endpoints
      properties:
        endpoints:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEndpoint'
      additionalProperties: false
