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


    **New here?** Start with the [Start here
    guide](https://docs.signo.fi/for-integrators): what you are building, the
    three ways in, how keys are issued, and the four calls with copy-paste
    examples. Then come back here for field-level detail.


    The Signo API exposes the same intent-to-transaction engine that powers the

    Signo 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. Signo 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 sg_...`). 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-09-08'
servers:
  - url: https://app.signo.fi/api/v1beta
    description: Production (beta)
security:
  - PartnerKey: []
tags:
  - name: Create an agent
    description: >-
      No API key needed. Any assistant can create a Notify or Prepare agent for
      a wallet it does not control, or request an early-access Execute
      permission for an eligible wallet. Signo stores an accepted draft inactive
      and returns a private review link; the wallet's owner claims it with one
      signature. These are also the two public MCP tools, signo_draft_agent and
      signo_draft_permission.
  - name: Shield
    description: >-
      Read a Shield proposal, its authorized permission and its execution runs.
      Partner key required; a proposal is readable only by the tenant that
      drafted it.
  - 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: Telegram
    description: >
      Wallet Telegram link state for partner Turn on buttons. Requires the
      `agents` scope

      (there is no separate telegram scope). Never returns chat ids or
      usernames.
  - name: Positions
    description: >
      A wallet's addressable positions, each row carrying the canonical
      composite `id` a

      `position` subject is named by. Requires the `agents` scope (the same
      scope the

      surface endpoints use). Not metered by credits.
  - name: Partner surfaces
    description: >
      Catalog-backed, contextual Signo agents embedded in partner pages. Keep
      the `sg_`

      key on the partner backend: catalog and resolve responses are
      browser-safe, while

      activate validates submitted values and returns a short-lived opaque URL.
  - 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. Send prior turns in `messages`, oldest first, as

      `{ role: "user" | "assistant", content: "..." }`. The server formats and

      bounds the history to the newest 6000 characters. The current message

      goes in `prompt`, not in `messages`. Only user turns can confirm a typed

      recipient or spender address; assistant replies cannot.


      Legacy `conversation_context` text is still accepted as descriptive
      context,

      but cannot confirm addresses from earlier turns. With legacy history,
      paste

      the address in the current `prompt`. If both fields are sent, `messages`

      takes precedence, including an empty array. Example follow-up request:

          prompt: "actually only move half of it"
          messages:
            - role: user
              content: "move my idle USDC into the best stable yield"
            - role: assistant
              content: "You hold 12,400 USDC idle on Ethereum. I can 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 `Signo-Schema-Version` header. Breaking changes ship under a new
      version prefix;

      additive changes bump the dated `schema_version` in place.


      **2026-09-08** — Signo headers and key naming; contextual partner
      surfaces, Telegram link status, and page context. Legacy key prefixes
      remain accepted during migration.


      **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.
x-tagGroups:
  - name: For AI assistants
    tags:
      - Create an agent
  - name: Partners · Transactions
    tags:
      - Planning
      - Execution
      - Streaming (SSE)
      - Multi-turn conversations
      - Clarifications
  - name: Partners · Agents
    tags:
      - Agents
      - Webhook delivery
      - Telegram
      - Shield
  - name: Partners · In-page surfaces
    tags:
      - Partner surfaces
      - Positions
  - name: Partners · Account
    tags:
      - Account
      - Versioning
paths:
  /agent-drafts:
    post:
      tags:
        - Create an agent
      operationId: draftAgent
      summary: Draft an inactive Notify or Prepare agent without an API key
      description: >-
        Returns a complete Notify or Prepare draft and private review URL. No
        active agent, subscription or spending authority is created. The owner
        opens the review URL, connects that wallet, links Telegram and signs
        once to create the agent. Prepare uses the existing per-fire unsigned
        planner; every transaction still requires owner approval. Action
        requests must explicitly choose prepare; otherwise they are refused as
        autonomy_mismatch, not silently downgraded. There is no clarification
        round trip: an ambiguous or unwatchable request is a 422 with the
        reason. Existing authenticated partner APIs remain available. Anonymous
        preparation is rate limited; drafts expire after 72 hours.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentDraftRequest'
      responses:
        '201':
          description: Inactive draft created
          content:
            application/json:
              schema:
                type: object
                required:
                  - proposal_id
                  - review_url
                  - expires_at
                  - status
                  - wallet
                  - summary
                  - cadence
                  - autonomy
                  - activation
                properties:
                  proposal_id:
                    type: string
                  expires_at:
                    type: integer
                    format: int64
                    description: Expiry in Unix milliseconds
                  review_url:
                    type: string
                    format: uri
                  status:
                    type: string
                    enum:
                      - draft
                  wallet:
                    type: string
                    nullable: true
                    description: The owner wallet
                    or null for a draft any wallet may claim: null
                  summary:
                    type: string
                  cadence:
                    type: string
                    enum:
                      - hourly
                      - daily
                      - weekly
                      - monthly
                    description: How often the condition is checked
                    or the schedule's cadence: null
                  autonomy:
                    type: string
                    enum:
                      - notify
                      - prepare
                  action:
                    type: string
                    nullable: true
                    description: >-
                      Null for Notify; action requests must explicitly choose
                      Prepare
                  activation:
                    type: string
        '400':
          description: Invalid input or unsupported fields
        '422':
          description: Needs clarification, unsupported condition, or autonomy mismatch
        '429':
          description: Public resource limit reached; Retry-After is provided
        '503':
          description: Compiler or required deployment configuration unavailable
  /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 `messages` as structured
        user/assistant

        messages, oldest first, with the current message in `prompt`. Structured

        user turns can confirm an address; assistant turns and legacy

        `conversation_context` cannot (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.


        Errors: `403 partner_not_configured` when the tenant's `partnerId` is
        not a

        partner this deployment supports, and `503 engine_unreachable` when the

        engine cannot be reached — retryable, and no credits are charged.
      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
                  messages:
                    - role: user
                      content: >-
                        move my idle USDC into the best stable yield across my
                        chains
                    - role: assistant
                      content: >-
                        You hold 12,400 USDC idle on Ethereum. I can 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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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.


        Errors: a tenant configured for a partner gets `403
        partner_scope_forbidden`

        when every leg of the requested plan belongs to a different protocol —
        the

        plan is refused whole, not trimmed. A tenant whose `partnerId` is not a

        partner this deployment supports gets `403 partner_not_configured`. When
        the

        planning engine itself cannot be reached the call returns

        `503 engine_unreachable`; it is safe to retry and costs no credits.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlanRequest'
      responses:
        '200':
          description: The ordered list of unsigned transaction steps.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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`.


        Errors: `503 engine_unreachable` when the encoding service cannot be
        reached.

        Retryable, and no credits are charged.
      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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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.


        Errors: `503 engine_unreachable` when the encoding service cannot be
        reached.

        Retryable, and no credits are charged.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimulateRequest'
      responses:
        '200':
          description: Per-step simulation results plus an aggregate.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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.


        Errors: `503 usage_unavailable` when the balance store cannot be read.

        Retryable; it never means a zero balance.
      responses:
        '200':
          description: Balance and recent usage.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetaResponse'
              examples:
                meta:
                  value:
                    schema_version: '2026-09-08'
                    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'
  /agent-catalog:
    get:
      operationId: listAgentCatalog
      tags:
        - Partner surfaces
      summary: List this partner's in-page agent definitions
      description: >
        Returns the authenticated tenant's versioned public surface definitions:
        copy,

        required subject identity, user fields, actions, and capabilities.
        Definitions

        are projected from Signo's agent catalog; private prompts and engine
        plans are

        never returned.


        Requires the `agents` scope and a partner configured on the tenant. Call
        from

        your backend with the server-only `sg_` key. The response is
        browser-safe and

        may be passed to your UI. Not credit-metered. A tenant with no
        configured

        catalog returns `403 partner_not_configured`.
      responses:
        '200':
          description: Public surface definitions available to the partner.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
            Cache-Control:
              schema:
                type: string
              description: Always `private, no-store`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurfaceCatalogResponse'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /surfaces/resolve:
    post:
      operationId: resolvePartnerSurface
      tags:
        - Partner surfaces
      summary: Resolve browser-safe state for an in-page surface
      description: >
        The default integration call. Send the current wallet and a stable page
        subject;

        Signo assembles the public definition, Telegram link state, and any
        existing

        in-page agent for the same tenant + wallet + catalog agent + normalized
        subject.


        Resolve never mints a one-time code or activation URL. Its response is
        safe to

        pass to browser components. `display_context` is presentation-only and
        never

        participates in identity, authorization, arming, or pricing. Call from
        your

        backend with the server-only `sg_` key. Not credit-metered.
        Surface-specific

        failures use `partner_not_configured`, `agent_not_found`, or

        `state_unavailable`; malformed input uses `invalid_request`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SurfaceResolveRequest'
            examples:
              agent:
                summary: A contextual market agent
                value:
                  agent: pendle-fixed-entry
                  wallet:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
                  subject:
                    kind: market
                    id: '0x4f3100000000000000000000000000000000ab12'
                    chain: ethereum
                  display_context:
                    title: PT-sUSDe
              linkOnly:
                summary: Telegram link state without an agent
                value:
                  intent: link
                  wallet:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
      responses:
        '200':
          description: Browser-safe surface state. No activation URL is minted.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
            Cache-Control:
              schema:
                type: string
              description: Always `private, no-store`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurfaceResolveResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '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'
  /surfaces/activate:
    post:
      operationId: activatePartnerSurface
      tags:
        - Partner surfaces
      summary: Validate submitted values and mint an opaque activation URL
      description: >
        Call only after the user submits the surface. Signo revalidates the
        agent,

        schema version, subject, action, and field values, then stores a
        short-lived

        server-owned prefill and returns its activation URL.


        Read `outcome` first. A wallet that already has a live Telegram link is

        armed in this call (`outcome: "activated"`, with the saved agent in

        `agent` and no URL): show the confirmation. Otherwise

        (`outcome: "requires_action"`) open `activation_url` exactly as
        returned.

        The `/go?p=...` token is opaque: do not parse, edit, cache, or
        reconstruct

        it. A later contract may change its encoding or remove query parameters

        entirely. Link-only activation uses `{ intent: "link", wallet }` and is

        always `requires_action`. Repeat submits are harmless: an identical

        configuration is a no-op, a changed one updates the same agent, a paused

        one resumes (`change`). Call from your backend with the server-only

        `sg_` key. Not credit-metered. Catalog/field/action mismatches return

        `invalid_configuration`; mint failures return `activation_unavailable`;

        a linked wallet whose agent could not be saved returns
        `activation_failed`;

        an unreadable Telegram link state returns `state_unavailable`; an

        unconfigured tenant returns `partner_not_configured`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SurfaceActivateRequest'
            examples:
              agent:
                value:
                  agent: pendle-fixed-entry
                  schema_version: '1'
                  wallet:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
                  subject:
                    kind: market
                    id: '0x4f3100000000000000000000000000000000ab12'
                    chain: ethereum
                  field_values:
                    dir: above
                    threshold: 8.5
                  action_id: propose-entry
              positionHealth:
                summary: Pendle LP position health (position subject, notify-only)
                value:
                  agent: pendle-lp-drift
                  schema_version: '1'
                  wallet:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
                  subject:
                    kind: position
                    id: >-
                      0x8f2a00000000000000000000000000000000d5|pendle|0x4f3100000000000000000000000000000000ab12|lp
                    chain: ethereum
                  field_values:
                    threshold: 4
                  action_id: notify
              positionValue:
                summary: >-
                  Pendle LP value move from setup (arm-time baseline,
                  notify-only)
                value:
                  agent: pendle-lp-value
                  schema_version: '1'
                  wallet:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
                  subject:
                    kind: position
                    id: >-
                      0x8f2a00000000000000000000000000000000d5|pendle|0x4f3100000000000000000000000000000000ab12|lp
                    chain: ethereum
                  field_values:
                    threshold: 10
                  action_id: notify
              loopSpread:
                summary: PT loop spread (wallet subject)
                value:
                  agent: pendle-loop-spread
                  schema_version: '1'
                  wallet:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
                  subject:
                    kind: wallet
                    chain: ethereum
                  field_values:
                    threshold: 4
                  action_id: notify
              linkOnly:
                value:
                  intent: link
                  wallet:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
      responses:
        '200':
          description: >-
            The activation outcome. `activated` when the wallet was already
            linked to Telegram (the agent is saved, nothing to open);
            `requires_action` with a short-lived opaque activation URL
            otherwise.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
            Cache-Control:
              schema:
                type: string
              description: Always `private, no-store`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurfaceActivationResponse'
        '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'
  /surfaces/review:
    post:
      operationId: createSurfaceReviewExchange
      tags:
        - Partner surfaces
      summary: Exchange a prepared result for a short-lived review URL
      description: >
        When `POST /surfaces/resolve` was called with `include: [result]` and
        the

        returned result has `review.available: true`, exchange that result id
        for a

        one-time review URL. The server verifies tenant, wallet, catalog agent,
        and

        normalized subject ownership before minting the exchange.


        Open `review_url` exactly as returned. It is short-lived and opaque and
        does

        not expose the underlying handoff token. This user review/signing
        transition

        is available through HTTP and the TypeScript SDK; it is intentionally
        not an

        MCP tool. Call from your backend with the server-only `sg_` key. Result
        state

        errors are `result_not_found`, `result_not_prepared`, and
        `result_expired`;

        transient exchange failures return `review_unavailable`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SurfaceReviewRequest'
            example:
              schema_version: '1'
              agent: pendle-fixed-entry
              wallet:
                address: '0x8f2a00000000000000000000000000000000d5'
                chain: ethereum
              subject:
                kind: market
                id: '0x4f3100000000000000000000000000000000ab12'
                chain: ethereum
              result_id: notification-1
      responses:
        '200':
          description: A short-lived one-time review URL.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
            Cache-Control:
              schema:
                type: string
              description: Always `private, no-store`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SurfaceReviewResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '404':
          description: >-
            The catalog agent or prepared result was not found for this tenant
            and subject.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The result exists but is not prepared for review.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: The prepared result expired before review was requested.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /agents:
    post:
      operationId: createAgent
      tags:
        - Agents
      summary: Create an 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

        Signo 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`.

        Defaults to **Notify**. Set `autonomy: prepare` to store a persistent
        action.

        Supply `action` for typed requests; plain-English can include the action
        in

        `request`. Explicit Prepare without an action returns 422. Execute
        requires

        a Shield permission and is not accepted here. Partner agents use
        webhooks or

        event polling without Telegram; keyless drafts require owner review and
        Telegram.

        Preparation delivers an unsigned handoff; every execution needs a wallet
        signature.

        Successful prepared plans use the existing 5-credit partner planning
        charge.

        Clarifications, failed plans and Notify fires are not charged.

        Optional `idempotency_key` deduplicates retries per tenant; changed
        bodies

        with the same key return 409 `idempotency_conflict`. Keys remain
        reserved after agent deletion; replaying a deleted agent returns 410
        `agent_deleted`.

        Invalid action/autonomy combinations return `action_required` or
        `action_requires_prepare`.

        `autonomy: execute` returns 422 `execute_requires_shield`.


        **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, an 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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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 an 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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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 an 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). Agents activated from a partner surface (`/surfaces/activate`)

        also refuse it with `400 invalid_patch`: their subject, field values and

        action were configured on the surface, so reconfigure them there or
        delete

        and recreate. `{ status }` and `DELETE` work on them like any agent.

        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 an agent
                value:
                  status: paused
              resume:
                summary: Resume an 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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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 an 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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
        '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 an 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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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, no credentials in the URL, and the hostname can't be a

        private/loopback/link-local/CGNAT/special-use IP literal (including the

        `169.254.169.254` cloud metadata address), `localhost`/`*.localhost`, or

        `*.local`/`*.internal`. A hostname must resolve, and every DNS answer
        must

        be a public address — checked here and again at every send; delivery
        then

        connects to the checked address (pinned, no second lookup), verifies TLS

        against the hostname, and never follows redirects. `400 invalid_url`

        otherwise, with the reason as `friendly_message`: `https_required`,

        `credentials_disallowed`, `private_ip`, `localhost`,
        `internal_hostname`,

        `private_dns_answer`, `no_dns_answers`, `dns_unresolved`, `dns_timeout`.


        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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          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:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /positions:
    get:
      operationId: listPositions
      tags:
        - Positions
      summary: A wallet's addressable positions, with canonical ids
      description: >
        Returns the positions in one wallet that a Signo agent can be armed on,
        each row

        carrying the canonical composite `id` already built. Read a row here and
        send its

        `id` straight to `POST /surfaces/resolve` and `POST /surfaces/activate`
        as the

        `position` subject. You never have to assemble the string yourself.


        **Addressable** means the position has a composite id of the documented
        form

        `<owner>|<protocol>|<reference>|<kind>`: `pendle` (`pt`, `yt`, `lp`) and
        `saffron`

        (`fixed`, `variable`) today. `owner` is the wallet holding the position
        and must

        equal the `wallet.address` you send on a surface call, which is exactly
        the wallet

        you queried here. Positions on other protocols are real holdings but
        cannot be a

        `position` subject, so they are not listed here.


        `kind` on a row is the kind inside the id, so a value you read is a
        value the

        surface endpoints accept.


        **An empty list is not always an answer.** Each protocol is read
        independently; one

        that fails or times out names itself in `unavailable` and sets
        `degraded: true`.

        Treat `positions: []` with a non-empty `unavailable` as "could not
        look", never as

        "this wallet holds nothing", and re-read before acting on it. When every
        source fails

        the response is `503 positions_unavailable` rather than an empty list.


        Requires the `agents` scope. Not credit-metered; the per-key rate limit
        applies.

        One wallet per call. Call it from your backend holding the `sg_` key.
      parameters:
        - name: wallet
          in: query
          required: true
          schema:
            type: string
          description: >-
            EVM `0x` address (40 hex). Pendle and Saffron positions are EVM-only
            today.
          example: '0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82'
        - name: protocol
          in: query
          required: false
          schema:
            type: string
            enum:
              - pendle
              - saffron
          description: Filter to one protocol with addressable positions.
          example: pendle
        - name: kind
          in: query
          required: false
          schema:
            type: string
            enum:
              - pt
              - yt
              - lp
              - fixed
              - variable
          description: >-
            Filter to one position kind. Pendle emits `pt`, `yt`, `lp`; Saffron
            emits `fixed`, `variable`.
          example: pt
        - name: chain
          in: query
          required: false
          schema:
            type: string
            enum:
              - ethereum
              - base
              - arbitrum
              - optimism
              - polygon
              - robinhood
              - hyperliquid
              - solana
          description: Filter to one chain slug, matching `GET /meta`'s `chains[]`.
          example: ethereum
      responses:
        '200':
          description: The wallet's addressable positions.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositionsResponse'
              examples:
                held:
                  summary: A Pendle PT and a Saffron fixed side
                  value:
                    wallet: '0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82'
                    positions:
                      - id: >-
                          0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82|pendle|0x3f5ea53d1160177445b1898afbb16da111182418|pt
                        owner: '0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82'
                        protocol: pendle
                        reference: '0x3f5ea53d1160177445b1898afbb16da111182418'
                        kind: pt
                        chain: ethereum
                        asset: sUSDe
                        value_usd: 12450.31
                        apy_pct: 11.42
                        days_to_expiry: 88
                      - id: >-
                          0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82|saffron|0x4f3100000000000000000000000000000000ab12|fixed
                        owner: '0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82'
                        protocol: saffron
                        reference: '0x4f3100000000000000000000000000000000ab12'
                        kind: fixed
                        chain: ethereum
                        asset: USDC
                        value_usd: 5000
                        apy_pct: 8.1
                        days_to_expiry: 21
                        claimable: 0
                        started: 1
                    degraded: false
                    unavailable: []
                empty:
                  summary: Nothing addressable in this wallet (both sources answered)
                  value:
                    wallet: '0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82'
                    positions: []
                    degraded: false
                    unavailable: []
                degraded:
                  summary: >-
                    Saffron answered, Pendle did not, so [] is not "holds
                    nothing"
                  value:
                    wallet: '0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82'
                    positions: []
                    degraded: true
                    unavailable:
                      - pendle
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/InvalidKey'
        '403':
          $ref: '#/components/responses/ScopeForbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: >
            Every position source failed, so no answer is possible. Body carries

            `error: positions_unavailable`. Retry; this is not a statement about
            the wallet.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /telegram/link-status:
    get:
      operationId: getTelegramLinkStatus
      tags:
        - Telegram
      summary: Whether a wallet has an active Telegram link
      description: >
        Returns `{ address, chain, linked }` for a single wallet so a partner
        button

        can say **Turn on** (`linked: true`) vs **Turn on with Telegram**

        (`linked: false`). A well-formed address with no row (or a disabled
        link)

        is `200` with `linked: false`, never `404`. Never returns chat ids or

        usernames.


        Requires the `agents` scope. Not credit-metered. Call this from your

        backend holding the `sg_` key — never from the browser.


        Address case is preserved (Solana is case-sensitive). Chain must be a

        slug from `GET /meta`'s `chains[]`.
      parameters:
        - name: address
          in: query
          required: true
          schema:
            type: string
          description: EVM `0x` address (40 hex) or Solana base58 address (32-44 chars).
          example: '0x8f2a00000000000000000000000000000000d5'
        - name: chain
          in: query
          required: true
          schema:
            type: string
            enum:
              - ethereum
              - base
              - arbitrum
              - optimism
              - polygon
              - robinhood
              - hyperliquid
              - solana
          description: Chain slug, matching `GET /meta`'s `chains[]`.
          example: ethereum
      responses:
        '200':
          description: Link state for the requested wallet.
          headers:
            Signo-Schema-Version:
              $ref: '#/components/headers/SignoSchemaVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TelegramLinkStatusResponse'
              examples:
                linked:
                  summary: Wallet has an active Telegram link
                  value:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
                    linked: true
                unlinked:
                  summary: No active link (missing or disabled)
                  value:
                    address: '0x8f2a00000000000000000000000000000000d5'
                    chain: ethereum
                    linked: false
        '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'
  /shield/proposals:
    post:
      tags:
        - Create an agent
      operationId: draftShieldPermission
      summary: >-
        Draft a Shield permission (an Execute agent) and return a private review
        URL; never authorizes spending
      description: >-
        The version 2 permission shape, with a partner key (agents scope) or
        without a key. Keyless calls are rate limited like agent drafts and
        stored under a public tenant that no key can read; the owner reads and
        authorizes on the review page. The named wallet must have Execute early
        access. An ineligible wallet gets 403 execute_team_only and nothing is
        stored. The returned message directs the owner to support@signo.fi. The
        protection short-form (a body with no schema_version) is retired and
        answers 400 with the mapping onto the permission shape. Free access.
        Budget and cap_per are amounts before fees; the Shield fee comes on top.
        cap_per may equal budget. The registered lifetime cap and token approval
        include a rounded-up fee buffer. Invalid budget arithmetic returns 400
        invalid_budget. A chain's native coin is not supported for Execute: wrap
        OKB to WOKB on X Layer and ETH to WETH on Arbitrum first. Owner wallet
        authorization is required on the review page; nothing can spend before
        the owner signs. An already-true condition can fire at the first hourly
        check. Start after Pause re-arms the agent and can fire at the next
        check without a new crossing. A check blocked by sizing, balance,
        approval, a venue stop or a service-wide execution pause can try again
        while the condition stays true. After a successful firing, one execution
        completes that trigger; an agent that stays active normally waits for a
        false reading and a new crossing. Public permission drafts do not opt
        into repay-from-collateral continuation.
      security:
        - {}
        - PartnerKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShieldPermissionRequest'
      responses:
        '200':
          description: >-
            Stored proposal_id, review_url, summary, limits, fee_bps and
            expires_at
        '400':
          description: Invalid input, including invalid_budget when cap_per exceeds budget
        '403':
          description: >-
            execute_team_only; the named wallet does not have Execute early
            access and nothing was stored
        '422':
          description: >-
            Protection cannot be prepared; includes compiler reason and
            explanation
  /shield/proposals/{id}:
    get:
      tags:
        - Shield
      operationId: getShieldProposal
      summary: Read a proposal created by this tenant
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: >-
            Tenant-scoped Shield data; permission is null and runs empty before
            authorization
        '404':
          description: Proposal not found for this tenant
  /shield/proposals/{id}/permission:
    get:
      tags:
        - Shield
      operationId: getShieldPermission
      summary: Read the permission authorized from this tenant proposal
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: >-
            Tenant-scoped Shield data; permission is null and runs empty before
            authorization
        '404':
          description: Proposal not found for this tenant
  /shield/proposals/{id}/runs:
    get:
      tags:
        - Shield
      operationId: listShieldRuns
      summary: Read bounded execution runs for this tenant proposal
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: >-
            Tenant-scoped Shield data; permission is null and runs empty before
            authorization
        '404':
          description: Proposal not found for this tenant
webhooks:
  agentFired:
    post:
      operationId: agentFiredWebhook
      tags:
        - Webhook delivery
      summary: An agent trigger pushed to your registered endpoint
      description: >
        Sent to every `active` endpoint registered for your tenant whenever an
        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-Signo-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-Signo-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-Signo-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 verifySignoSignature(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: sg_...
      description: |
        A partner API key, presented as `Authorization: Bearer sg_...`. 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 signo.fi.
  headers:
    SignoSchemaVersion:
      description: The public contract version this response was built against.
      schema:
        type: string
        example: '2026-09-08'
    SignoRequestId:
      description: >-
        The id for this request. Send `Signo-Request-Id` and we keep your value
        so your logs and ours share one key; omit it and we mint one. Present on
        every response, streaming included, and on the error body as
        `request_id`. Log it: it is what turns a support question into one
        lookup. Accepted shape: 8-128 characters of `A-Za-z0-9._:-`; anything
        else is replaced with a generated id.
      schema:
        type: string
        example: 454c6f34-fc9f-4a8c-ae75-797f41b0dd5d
  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 Signo's side. The request was well-formed; retrying
        is reasonable, and a persistent `internal_error` is worth reporting.
      headers:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: invalid_key
            friendly_message: >-
              Missing or malformed API key. Pass it as `Authorization: Bearer
              sg_...`.
            issues: []
    InsufficientCredits:
      description: The tenant's credit balance is below the call cost.
      headers:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      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:
        Signo-Schema-Version:
          $ref: '#/components/headers/SignoSchemaVersion'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: agent_not_found
            friendly_message: No agent with that id.
            issues: []
  schemas:
    AgentDraftRequest:
      type: object
      additionalProperties: false
      required:
        - request
      properties:
        wallet:
          type: string
          description: >-
            Optional intended owner EVM or Solana address. Given, only this
            wallet can claim the draft. Omitted, the draft is bound to the first
            wallet that signs on the review page.
        request:
          type: string
          minLength: 8
          maxLength: 2000
          description: >-
            Complete condition or recurring schedule and desired notification or
            Prepare action
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          default: notify
          description: >-
            Prepare must be explicit for action requests; Execute uses Shield
            permissions
    ShieldPermissionRequest:
      type: object
      description: >-
        Draft an Execute permission on X Layer or Arbitrum One; the owner must
        authorize it separately. New Execute setup is in early access: an
        ineligible wallet gets 403 execute_team_only and no proposal is stored.
        Asset fields accept a curated Signo symbol, a symbol on the chain's
        token list (OKX on X Layer, LI.FI on Arbitrum), or an ERC-20 address.
        Curated names resolve first; USDT aliases USDT0. If several listed
        tokens share a symbol, needs_clarification returns candidate addresses
        and token-page links so the caller can choose an address and retry.
        These symbols are not an allowlist for transfers or swaps; Aave actions
        require an Aave reserve. Drafting does not require the wallet to hold
        the budget yet.
      additionalProperties: false
      required:
        - schema_version
        - wallet
        - chain_id
        - action
        - trigger
        - budget
        - cap_per
      properties:
        schema_version:
          type: integer
          enum:
            - 2
        wallet:
          type: string
          pattern: ^0x[0-9a-fA-F]{40}$
        chain_id:
          type: integer
          enum:
            - 196
            - 42161
          description: >-
            X Layer (196) or Arbitrum One (42161). GET /api/shield/compile lists
            the live chains.
        budget:
          type: string
          description: >-
            Lifetime input-token budget before fees. The Shield fee comes on
            top; the registered lifetime cap and token approval include a
            rounded-up fee buffer. Must be at least cap_per, and may equal it.
            The wallet need not hold it at draft time; each firing checks its
            balance.
        cap_per:
          type: string
          description: >-
            Per-execution input-token cap before fees. Must be positive and no
            greater than budget (it may equal budget). The Shield fee comes on
            top.
        expiry_days:
          type: integer
          minimum: 1
          maximum: 90
          default: 30
        trigger:
          oneOf:
            - type: object
              additionalProperties: false
              required:
                - kind
                - comparator
                - value
              properties:
                kind:
                  type: string
                  enum:
                    - health_factor
                comparator:
                  type: string
                  enum:
                    - <
                    - <=
                    - '>'
                    - '>='
                value:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 100
            - type: object
              additionalProperties: false
              required:
                - kind
                - asset
                - comparator
                - value
              properties:
                kind:
                  type: string
                  enum:
                    - token_balance
                asset:
                  type: string
                  description: >-
                    Token symbol or ERC-20 address, resolved as described above;
                    balance is in token units on the chain of the permission for
                    the owner wallet.
                comparator:
                  type: string
                  enum:
                    - <
                    - <=
                    - '>'
                    - '>='
                value:
                  type: number
                  minimum: 0
                  maximum: 9007199254740991
            - type: object
              additionalProperties: false
              required:
                - kind
                - asset
                - comparator
                - value
              properties:
                kind:
                  type: string
                  enum:
                    - price
                asset:
                  type: string
                  description: >-
                    Token symbol or ERC-20 address with an Aave oracle price on
                    the chain of the permission (Chainlink-backed); the current
                    USD price, never a move over a window.
                comparator:
                  type: string
                  enum:
                    - <
                    - <=
                    - '>'
                    - '>='
                value:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 1000000000000
            - type: object
              additionalProperties: false
              required:
                - kind
                - request
              properties:
                kind:
                  type: string
                  enum:
                    - condition
                request:
                  type: string
                  minLength: 8
                  maxLength: 2000
                  description: >-
                    A supported plain-English Execute condition. Compiled by the
                    same compiler as the app; the part the Shield can read is
                    pinned and re-checked on chain, the rest is decided by the
                    hourly check. Execute is not available for the app Portfolio
                    move trigger; boxed Pendle PT loops are notify-only today. A
                    recurring schedule is not an Execute trigger.
        action:
          oneOf:
            - type: object
              additionalProperties: false
              required:
                - kind
                - asset
                - recipient
              properties:
                kind:
                  type: string
                  enum:
                    - transfer
                asset:
                  type: string
                recipient:
                  type: string
                  pattern: ^0x[0-9a-fA-F]{40}$
            - type: object
              additionalProperties: false
              required:
                - kind
                - asset
                - token_out
              description: Provide exactly one of min_out or max_slippage_bps.
              properties:
                kind:
                  type: string
                  enum:
                    - swap
                asset:
                  type: string
                token_out:
                  type: string
                min_out:
                  type: string
                  description: Positive output-token floor per execution.
                max_slippage_bps:
                  type: integer
                  minimum: 1
                  maximum: 1000
            - type: object
              additionalProperties: false
              required:
                - kind
                - vault
              properties:
                kind:
                  type: string
                  enum:
                    - vault
                vault:
                  type: string
                  pattern: ^0x[0-9a-fA-F]{40}$
                asset:
                  type: string
                max_slippage_bps:
                  type: integer
                  minimum: 0
                  maximum: 1000
                  default: 0
            - type: object
              additionalProperties: false
              required:
                - kind
                - asset
              properties:
                kind:
                  type: string
                  enum:
                    - supply
                    - repay_from_wallet
                  description: >-
                    supply into Aave, or repay_from_wallet: repay Aave debt from
                    the wallet balance. The pre-2026-09-25 name "wallet" is
                    still accepted.
                asset:
                  type: string
            - type: object
              additionalProperties: false
              required:
                - kind
                - collateral
                - debt
                - target_health_factor
              properties:
                kind:
                  type: string
                  enum:
                    - repay_from_collateral
                  description: >-
                    Repay Aave debt by selling collateral down to
                    target_health_factor. The pre-2026-09-25 name "collateral"
                    is still accepted.
                collateral:
                  type: string
                debt:
                  type: string
                target_health_factor:
                  type: number
                  exclusiveMinimum: 1
                  maximum: 10
                max_slippage_bps:
                  type: integer
                  minimum: 1
                  maximum: 1000
    Error:
      type: object
      description: The single error envelope every 4xx/5xx response uses.
      required:
        - error
        - friendly_message
        - issues
        - request_id
      properties:
        request_id:
          type: string
          description: >-
            The id for the failed request — the same value as the
            `Signo-Request-Id` response header. Quote it in a support request.
          example: 454c6f34-fc9f-4a8c-ae75-797f41b0dd5d
        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 Signo fetches them.
          items:
            $ref: '#/components/schemas/Balance'
        page_context:
          type: object
          description: >-
            Descriptive page facts; partner identity and existing scope policy
            are derived from the authenticated tenant. Not a permissions
            override.
          required:
            - surface
          additionalProperties: false
          properties:
            surface:
              type: string
              minLength: 1
              maxLength: 200
            chain:
              type: string
              minLength: 1
              maxLength: 40
            market:
              type: string
              minLength: 1
              maxLength: 200
            position:
              type: string
              minLength: 1
              maxLength: 200
            asset:
              type: string
              minLength: 1
              maxLength: 80
        messages:
          type: array
          maxItems: 100
          description: |
            Prior turns, oldest first; the current message belongs in `prompt`.
            The server formats role labels and keeps the newest history within
            6000 characters. Only user turns can confirm a recipient or spender
            address. Takes precedence over `conversation_context`, including an
            empty array. Invalid structured history is rejected, not ignored.
          items:
            type: object
            required:
              - role
              - content
            additionalProperties: false
            properties:
              role:
                type: string
                enum:
                  - user
                  - assistant
              content:
                type: string
                maxLength: 64000
        conversation_context:
          type: string
          description: >
            Legacy descriptive context. Addresses here do not count as typed by

            the user; paste the address in the current `prompt`, or migrate to

            `messages`. Ignored when `messages` is supplied.


            Prior turns of the conversation, for multi-turn planning: the prior
            turns

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

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

            in `prompt`, never in this block.


            Longer than 6000 characters is accepted, not rejected: the server
            keeps

            the NEWEST end and drops whole turns off the front until it fits, so
            send

            what you have rather than truncating it yourself. If you do trim,
            trim

            the OLDEST turns — a client that cuts the newest end throws away the
            part

            the planner most needs.
          example: >-
            User: move my idle USDC into the best stable yield

            Signo: 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).
        unsupported_code:
          type: string
          enum:
            - invalid_request
            - unknown_target
            - step_type_unsupported
            - protocol_unsupported
            - quote_failed
            - target_changed
            - step_incomplete
          description: >-
            The machine-readable half of `unsupported_reason`, present on every
            `ok: false`. Branch on this instead of parsing the prose, which is
            written for people and may change. `quote_failed` is usually worth
            one retry; `target_changed` means the protocol answered with a
            different contract than the user reviewed, so re-plan; the rest
            describe the step or the request and will not improve on retry.
        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
    TelegramLinkStatusResponse:
      type: object
      required:
        - address
        - chain
        - linked
      properties:
        address:
          type: string
          description: The wallet address as requested (original case preserved).
          example: '0x8f2a00000000000000000000000000000000d5'
        chain:
          type: string
          description: The chain slug as requested.
          example: ethereum
        linked:
          type: boolean
          description: >
            True when the wallet has an active Telegram link. False when there
            is no row or the link is disabled. Never accompanied by chat ids or
            usernames.
      additionalProperties: false
    Position:
      type: object
      required:
        - id
        - owner
        - protocol
        - reference
        - kind
        - chain
        - asset
        - value_usd
      description: >
        One addressable position. `id` is the canonical composite subject id;
        `owner`,

        `protocol`, `reference` and `kind` are its parts, split out so you do
        not have to

        parse the string. Send `id` verbatim as a `position` subject.
      properties:
        id:
          type: string
          description: Canonical composite id, `<owner>|<protocol>|<reference>|<kind>`.
          example: >-
            0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82|pendle|0x3f5ea53d1160177445b1898afbb16da111182418|pt
        owner:
          type: string
          description: >-
            The wallet holding the position, lower-cased. Equals the `wallet`
            you queried.
        protocol:
          type: string
          enum:
            - pendle
            - saffron
        reference:
          type: string
          description: >-
            The protocol's own key, a Pendle market address or a Saffron vault
            address.
        kind:
          type: string
          enum:
            - pt
            - yt
            - lp
            - fixed
            - variable
          description: >-
            Which leg of that market or vault. The same value the surface
            endpoints accept.
        chain:
          type: string
          description: >-
            Canonical chain slug. Chain is a separate part of a subject, so
            carry it alongside `id`.
        asset:
          type: string
          description: The underlying asset symbol, for display.
        value_usd:
          type: number
          description: Position value in USD at read time.
        apy_pct:
          type: number
          description: >-
            Annualized rate as a percent (11.42 means 11.42%). Omitted, never
            zeroed, when the protocol metadata was unavailable.
        days_to_expiry:
          type: integer
          description: >-
            Whole days until maturity. Omitted when the position does not
            mature.
        claimable:
          type: integer
          description: Saffron. 1 when the fixed premium is claimable, else 0.
        started:
          type: integer
          description: >-
            Saffron. 1 while the vault is active (started and not ended), 0
            while the offer is open or after it ended.
        accepted_hours_ago:
          type: number
          description: >-
            Saffron. Hours since the vault started. Present only while it is
            active.
        fill_pct:
          type: number
          description: Saffron. Variable-side fill, 0 to 100, while the offer is open.
      additionalProperties: false
    PositionsResponse:
      type: object
      required:
        - wallet
        - positions
        - degraded
        - unavailable
      properties:
        wallet:
          type: string
          description: The wallet that was read, lower-cased.
        positions:
          type: array
          items:
            $ref: '#/components/schemas/Position'
        degraded:
          type: boolean
          description: >-
            True when at least one source could not be read. `positions` is then
            partial, and an empty array is NOT evidence the wallet holds
            nothing.
        unavailable:
          type: array
          description: The protocols that failed or timed out on this call.
          items:
            type: string
            enum:
              - pendle
              - saffron
      additionalProperties: false
    SurfaceWallet:
      type: object
      required:
        - address
        - chain
      description: >-
        The wallet whose Telegram link and in-page agent state the surface
        represents.
      properties:
        address:
          type: string
          description: >-
            EVM `0x` address or Solana base58 address. EVM addresses are
            normalized to lowercase.
        chain:
          type: string
          description: Canonical chain slug from `GET /meta`.
      additionalProperties: false
    SurfaceSubject:
      description: >
        Stable page identity used to arm and deduplicate an in-page agent. The
        subject kind

        vocabulary is closed. Never substitute `display_context` for a missing
        id.


        A `wallet` subject has no resource id. Identity is tenant + wallet +
        agent +

        `wallet:<chain>`. `chain` is required and must match `wallet.chain`. One
        wallet-level

        agent of a given type per wallet per chain. Do not send `subject.id`.


        A `position` id is composite: `<owner>|<protocol>|<reference>|<kind>`,
        where `owner`

        is the wallet holding the position and must equal the `wallet.address`
        sent on the

        same call, `reference` is the protocol's own key (a Pendle market
        address, a Saffron

        vault address), and `kind` names the leg. Supported: `pendle` (`pt`,
        `yt`, `lp`) and

        `saffron` (`fixed`, `variable`). EVM addresses in `owner` and
        `reference` are

        lower-cased, as is `kind`, so an id you build matches the one echoed
        back. A

        malformed id, an unsupported protocol or kind, or an owner other than
        the request

        wallet is rejected with `invalid_request` on `subject`. The SDK exports
        `positionId`

        and `parsePositionId` rather than leaving partners to assemble the
        string.
      oneOf:
        - type: object
          required:
            - kind
            - id
            - chain
          properties:
            kind:
              type: string
              enum:
                - market
                - vault
                - position
                - asset
            id:
              type: string
              description: >-
                Stable protocol/resource id. EVM ids are normalized to
                lowercase. For `kind: position` this is the composite id
                described above, not a bare address.
              example: >-
                0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82|pendle|0x3f5ea53d1160177445b1898afbb16da111182418|pt
            chain:
              type: string
              description: Canonical chain slug from `GET /meta`.
          additionalProperties: false
        - type: object
          required:
            - kind
            - chain
          properties:
            kind:
              type: string
              enum:
                - wallet
            chain:
              type: string
              description: >-
                Canonical chain slug from `GET /meta`. Required. Must match
                `wallet.chain`.
          additionalProperties: false
    SurfaceDisplayContext:
      type: object
      description: >-
        Presentation-only page context. Never used for identity, authorization,
        arming, or pricing.
      properties:
        title:
          type: string
          maxLength: 200
        subtitle:
          type: string
          maxLength: 400
        image_url:
          type: string
          format: uri
          maxLength: 2000
        metadata:
          type: object
          additionalProperties:
            oneOf:
              - type: string
                maxLength: 500
              - type: number
              - type: boolean
      additionalProperties: false
    SurfaceFieldOption:
      type: object
      required:
        - value
        - label
      properties:
        value:
          oneOf:
            - type: string
            - type: number
        label:
          type: string
      additionalProperties: false
    SurfaceField:
      type: object
      required:
        - id
        - type
        - label
        - required
        - default_value
      properties:
        id:
          type: string
          description: Stable field id to use as the `field_values` key during activation.
        type:
          type: string
          enum:
            - number
            - direction
            - enum
          description: >-
            Unknown types must fail visibly rather than rendering an invented
            control.
        label:
          type: string
        required:
          type: boolean
        default_value:
          oneOf:
            - type: string
            - type: number
        options:
          type: array
          items:
            $ref: '#/components/schemas/SurfaceFieldOption'
        suggestions:
          type: array
          items:
            oneOf:
              - type: string
              - type: number
        step:
          type: number
        suffix:
          type: string
      additionalProperties: false
    SurfaceAction:
      type: object
      required:
        - id
        - label
        - capability
      properties:
        id:
          type: string
          description: Stable action id to send as `action_id` during activation.
        label:
          type: string
        capability:
          type: string
          enum:
            - notify
            - prepare
      additionalProperties: false
    SurfaceDefinition:
      type: object
      required:
        - agent
        - schema_version
        - title
        - description
        - subject
        - capabilities
        - fields
        - actions
        - copy
      properties:
        agent:
          type: string
          description: Partner-scoped catalog id, not a component or protocol type.
        schema_version:
          type: string
          enum:
            - '1'
        title:
          type: string
        description:
          type: string
        subject:
          type: object
          required:
            - kind
            - required
          properties:
            kind:
              type: string
              enum:
                - market
                - vault
                - position
                - asset
                - wallet
            required:
              type: array
              items:
                type: string
                enum:
                  - id
                  - chain
          additionalProperties: false
        capabilities:
          type: array
          items:
            type: string
            enum:
              - notify
              - prepare
        fields:
          type: array
          items:
            $ref: '#/components/schemas/SurfaceField'
        actions:
          type: array
          items:
            $ref: '#/components/schemas/SurfaceAction'
        copy:
          type: object
          required:
            - trigger
            - submit
          properties:
            trigger:
              type: string
            submit:
              type: string
          additionalProperties: false
      additionalProperties: false
    SurfaceCatalogResponse:
      type: object
      required:
        - schema_version
        - agents
      properties:
        schema_version:
          type: string
          enum:
            - '1'
        agents:
          type: array
          items:
            $ref: '#/components/schemas/SurfaceDefinition'
      additionalProperties: false
    SurfaceAgentResolveRequest:
      type: object
      required:
        - agent
        - wallet
        - subject
      properties:
        agent:
          type: string
          maxLength: 100
        wallet:
          $ref: '#/components/schemas/SurfaceWallet'
        subject:
          $ref: '#/components/schemas/SurfaceSubject'
        display_context:
          $ref: '#/components/schemas/SurfaceDisplayContext'
        include:
          type: array
          maxItems: 1
          items:
            type: string
            enum:
              - result
          description: >-
            Optionally include the latest structured prepared result when one
            exists.
      additionalProperties: false
    SurfaceLinkResolveRequest:
      type: object
      required:
        - intent
        - wallet
      properties:
        intent:
          type: string
          enum:
            - link
        wallet:
          $ref: '#/components/schemas/SurfaceWallet'
        display_context:
          $ref: '#/components/schemas/SurfaceDisplayContext'
      additionalProperties: false
    SurfaceResolveRequest:
      oneOf:
        - $ref: '#/components/schemas/SurfaceAgentResolveRequest'
        - $ref: '#/components/schemas/SurfaceLinkResolveRequest'
    SurfaceExistingAgent:
      type: object
      required:
        - id
        - status
        - field_values
        - action_id
        - activated_at
        - rendered
      properties:
        rendered:
          type: string
          description: >-
            The saved condition in words, e.g. "SIERRA yields above 10%". The
            same string `GET /agents/{id}` returns; display text, never parse
            it.
        id:
          type: string
          description: >-
            Public agent id (`w_condition_...`). Pass it straight to
            `/v1beta/agents/{id}` to read, pause or delete this agent.
          pattern: ^w_(alert|reminder|condition)_[A-Za-z0-9_-]+$
        status:
          type: string
          enum:
            - active
            - paused
            - degraded
        field_values:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
        action_id:
          type: string
        activated_at:
          type: integer
          description: Unix epoch milliseconds.
      additionalProperties: false
    SurfaceResultDetail:
      description: >-
        A typed, server-projected fact. Partner HTML and arbitrary detail types
        are never accepted.
      oneOf:
        - type: object
          required:
            - type
            - label
            - value
            - unit
          properties:
            type:
              type: string
              enum:
                - rate
            label:
              type: string
            value:
              type: number
            unit:
              type: string
              enum:
                - percent
          additionalProperties: false
        - type: object
          required:
            - type
            - label
            - value
          properties:
            type:
              type: string
              enum:
                - term
            label:
              type: string
            value:
              type: string
          additionalProperties: false
        - type: object
          required:
            - type
            - label
            - value
          properties:
            type:
              type: string
              enum:
                - amount
            label:
              type: string
            value:
              type: string
            asset:
              type: string
          additionalProperties: false
        - type: object
          required:
            - type
            - label
            - value
          properties:
            type:
              type: string
              enum:
                - destination
            label:
              type: string
            value:
              type: string
          additionalProperties: false
    SurfaceResult:
      type: object
      required:
        - schema_version
        - id
        - status
        - summary
        - prepared_at
        - expires_at
        - plan_preview
        - review
      description: >-
        Structured prepared outcome projected by Signo; contains no private
        prompt, raw intents, or handoff token.
      properties:
        schema_version:
          type: string
          enum:
            - '1'
        id:
          type: string
          description: Opaque result id to pass to `/surfaces/review`.
        status:
          type: string
          enum:
            - prepared
            - expired
        summary:
          type: string
        rationale:
          type: string
        prepared_at:
          type: integer
          description: Unix epoch milliseconds.
        expires_at:
          type: integer
          description: Unix epoch milliseconds.
        plan_preview:
          type: object
          required:
            - steps
          properties:
            steps:
              type: array
              items:
                type: object
                required:
                  - action
                  - label
                  - details
                properties:
                  action:
                    type: string
                  label:
                    type: string
                  details:
                    type: array
                    items:
                      $ref: '#/components/schemas/SurfaceResultDetail'
                additionalProperties: false
          additionalProperties: false
        review:
          type: object
          required:
            - available
          properties:
            available:
              type: boolean
          additionalProperties: false
      additionalProperties: false
    SurfaceAgentResolveResponse:
      type: object
      required:
        - intent
        - wallet
        - linked
        - definition
        - subject
      properties:
        intent:
          type: string
          enum:
            - agent
        wallet:
          $ref: '#/components/schemas/SurfaceWallet'
        linked:
          type: boolean
        definition:
          $ref: '#/components/schemas/SurfaceDefinition'
        subject:
          $ref: '#/components/schemas/SurfaceSubject'
        display_context:
          $ref: '#/components/schemas/SurfaceDisplayContext'
        existing:
          $ref: '#/components/schemas/SurfaceExistingAgent'
        result:
          description: >-
            Present only when requested with `include: [result]`; null when no
            result exists.
          oneOf:
            - $ref: '#/components/schemas/SurfaceResult'
            - type: 'null'
      additionalProperties: false
    SurfaceLinkResolveResponse:
      type: object
      required:
        - intent
        - wallet
        - linked
      properties:
        intent:
          type: string
          enum:
            - link
        wallet:
          $ref: '#/components/schemas/SurfaceWallet'
        linked:
          type: boolean
        display_context:
          $ref: '#/components/schemas/SurfaceDisplayContext'
      additionalProperties: false
    SurfaceResolveResponse:
      oneOf:
        - $ref: '#/components/schemas/SurfaceAgentResolveResponse'
        - $ref: '#/components/schemas/SurfaceLinkResolveResponse'
    SurfaceAgentActivateRequest:
      type: object
      required:
        - agent
        - schema_version
        - wallet
        - subject
      properties:
        agent:
          type: string
          maxLength: 100
        schema_version:
          type: string
          enum:
            - '1'
        wallet:
          $ref: '#/components/schemas/SurfaceWallet'
        subject:
          $ref: '#/components/schemas/SurfaceSubject'
        display_context:
          $ref: '#/components/schemas/SurfaceDisplayContext'
        field_values:
          type: object
          description: >-
            User values keyed by ids in the resolved definition's `fields`
            array.
          additionalProperties:
            oneOf:
              - type: string
              - type: number
        action_id:
          type: string
          description: Stable id from the resolved definition's `actions` array.
      additionalProperties: false
    SurfaceActivateRequest:
      oneOf:
        - $ref: '#/components/schemas/SurfaceAgentActivateRequest'
        - $ref: '#/components/schemas/SurfaceLinkResolveRequest'
    SurfaceActivationResponse:
      oneOf:
        - $ref: '#/components/schemas/SurfaceActivatedResponse'
        - $ref: '#/components/schemas/SurfaceActivationRequiredResponse'
      discriminator:
        propertyName: outcome
    SurfaceActivatedResponse:
      type: object
      description: >-
        The wallet already had a live Telegram link, so the agent was saved
        server-side in this call. Nothing to open: show `agent`. Repeat submits
        are harmless (an identical configuration is a no-op, a relabel refreshes
        the words only, a changed configuration updates the same agent, a paused
        one resumes).
      required:
        - outcome
        - intent
        - agent
        - change
        - rendered
      properties:
        outcome:
          type: string
          enum:
            - activated
        intent:
          type: string
          enum:
            - agent
        agent:
          $ref: '#/components/schemas/SurfaceExistingAgent'
        change:
          type: string
          enum:
            - created
            - updated
            - resumed
            - unchanged
          description: What this submit did to the agent.
        rendered:
          type: string
          description: The saved condition in words, e.g. "SIERRA yields above 10%".
      additionalProperties: false
    SurfaceActivationRequiredResponse:
      type: object
      description: >-
        The user must open `activation_url` to connect Telegram; the agent is
        saved when that completes. Link-only activation (`intent: link`) is
        always this outcome.
      required:
        - outcome
        - intent
        - activation_url
        - expires_at
      properties:
        outcome:
          type: string
          enum:
            - requires_action
        intent:
          type: string
          enum:
            - agent
            - link
        activation_url:
          type: string
          format: uri
          description: >-
            Short-lived opaque URL. Open as-is; never parse, edit, cache, or
            reconstruct it.
          example: https://signo.fi/go?p=pd_opaque
        expires_at:
          type: integer
          description: Unix epoch milliseconds.
      additionalProperties: false
    SurfaceReviewRequest:
      type: object
      required:
        - schema_version
        - agent
        - wallet
        - subject
        - result_id
      properties:
        schema_version:
          type: string
          enum:
            - '1'
        agent:
          type: string
          maxLength: 100
        wallet:
          $ref: '#/components/schemas/SurfaceWallet'
        subject:
          $ref: '#/components/schemas/SurfaceSubject'
        result_id:
          type: string
          maxLength: 100
          description: Opaque id returned in the resolved `result` envelope.
      additionalProperties: false
    SurfaceReviewResponse:
      type: object
      required:
        - review_url
        - expires_at
      properties:
        review_url:
          type: string
          format: uri
          description: >-
            Short-lived one-time URL. Open as-is; the underlying handoff token
            is never exposed.
          example: https://signo.fi/go?r=sr_opaque
        expires_at:
          type: integer
          description: Unix epoch milliseconds.
      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-09-08'
        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:
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          description: >-
            Defaults to notify on creation; omitted on update preserves existing
            autonomy.
        action:
          type: string
          minLength: 3
          maxLength: 2000
          description: Action to prepare at trigger time. Requires autonomy prepare.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 128
          description: Tenant-scoped retry key. Same key with a changed body returns 409.
        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:
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          description: >-
            Defaults to notify on creation; omitted on update preserves existing
            autonomy.
        action:
          type: string
          minLength: 3
          maxLength: 2000
          description: Action to prepare at trigger time. Requires autonomy prepare.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 128
          description: Tenant-scoped retry key. Same key with a changed body returns 409.
        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:
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          description: >-
            Defaults to notify on creation; omitted on update preserves existing
            autonomy.
        action:
          type: string
          minLength: 3
          maxLength: 2000
          description: Action to prepare at trigger time. Requires autonomy prepare.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 128
          description: Tenant-scoped retry key. Same key with a changed body returns 409.
        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:
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          description: >-
            Defaults to notify on creation; omitted on update preserves existing
            autonomy.
        action:
          type: string
          minLength: 3
          maxLength: 2000
          description: Action to prepare at trigger time. Requires autonomy prepare.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 128
          description: Tenant-scoped retry key. Same key with a changed body returns 409.
        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:
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          description: >-
            Defaults to notify on creation; omitted on update preserves existing
            autonomy.
        action:
          type: string
          minLength: 3
          maxLength: 2000
          description: Action to prepare at trigger time. Requires autonomy prepare.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 128
          description: Tenant-scoped retry key. Same key with a changed body returns 409.
        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 check-in or Prepare action. 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:
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          description: >-
            Defaults to notify on creation; omitted on update preserves existing
            autonomy.
        action:
          type: string
          minLength: 3
          maxLength: 2000
          description: Action to prepare at trigger time. Requires autonomy prepare.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 128
          description: Tenant-scoped retry key. Same key with a changed body returns 409.
        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 an 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:
        autonomy:
          type: string
          enum:
            - notify
            - prepare
          description: >-
            Defaults to notify on creation; omitted on update preserves existing
            autonomy.
        action:
          type: string
          nullable: true
          minLength: 3
          maxLength: 2000
          description: Action to prepare at trigger time. Requires autonomy prepare.
        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:
            autonomy:
              type: string
              enum:
                - notify
                - prepare
              description: >-
                Defaults to notify on creation; omitted on update preserves
                existing autonomy.
            action:
              type: string
              minLength: 3
              maxLength: 2000
              description: Action to prepare at trigger time. Requires autonomy prepare.
            request:
              type: string
              minLength: 3
              maxLength: 500
          additionalProperties: false
        - type: object
          description: >-
            Update action/autonomy on any tenant-owned kind; omitted autonomy
            preserves the existing mode.
          minProperties: 1
          properties:
            autonomy:
              type: string
              enum:
                - notify
                - prepare
              description: >-
                Defaults to notify on creation; omitted on update preserves
                existing autonomy.
            action:
              type: string
              minLength: 3
              maxLength: 2000
              description: Action to prepare at trigger time. Requires autonomy prepare.
          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
    AgentPreparation:
      type: object
      description: >-
        Published after planning completes. Original trigger time is
        triggered_at; event created_at is publication time. No transaction is
        signed or broadcast. Review quotes refresh on opening the handoff.
      properties:
        status:
          type: string
          enum:
            - ready
            - clarification
            - informational
            - failed
            - cancelled
        triggered_at:
          type: number
        action:
          type: string
        reasoning:
          type: string
        intents:
          type: array
          items:
            type: object
        review_url:
          type: string
          description: >-
            Private bearer review link. Do not publish it. Handoffs expire after
            seven days.
        built_at:
          type: number
        error:
          type: string
        requires_signature:
          type: boolean
          enum:
            - true
    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:
        preparation:
          $ref: '#/components/schemas/AgentPreparation'
        id:
          type: string
          description: >-
            `evt_<internal notification id>` — identical to the pushed webhook's
            `id` / `X-Signo-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:
        preparation:
          $ref: '#/components/schemas/AgentPreparation'
        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
