Skip to main content

Signo API (1beta)

Download OpenAPI specification:Download

Turn a plain-English outcome into a ready-to-sign, multi-step, cross-chain transaction plan.

Overview

New here? Start with the Start here guide: 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.

Create an agent

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.

Draft an inactive Notify or Prepare agent without an API key

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.

Request Body schema: application/json
required
wallet
string

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
required
string [ 8 .. 2000 ] characters

Complete condition or recurring schedule and desired notification or Prepare action

autonomy
string
Default: "notify"
Enum: "notify" "prepare"

Prepare must be explicit for action requests; Execute uses Shield permissions

Responses

Request samples

Content type
application/json
{
  • "wallet": "string",
  • "request": "stringst",
  • "autonomy": "notify"
}

Response samples

Content type
application/json
{
  • "proposal_id": "string",
  • "expires_at": 0,
  • "review_url": "http://example.com",
  • "status": "draft",
  • "wallet": "string",
  • "summary": "string",
  • "cadence": "hourly",
  • "autonomy": "notify",
  • "action": "string",
  • "activation": "string"
}

Draft a Shield permission (an Execute agent) and return a private review URL; never authorizes spending

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.

Authorizations:
NonePartnerKey
Request Body schema: application/json
required
schema_version
required
integer
Value: 2
wallet
required
string^0x[0-9a-fA-F]{40}$
chain_id
required
integer
Enum: 196 42161

X Layer (196) or Arbitrum One (42161). GET /api/shield/compile lists the live chains.

budget
required
string

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
required
string

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
integer [ 1 .. 90 ]
Default: 30
required
object or object or object or object
required
object or object or object or object or object

Responses

Request samples

Content type
application/json
{
  • "schema_version": 2,
  • "wallet": "string",
  • "chain_id": 196,
  • "budget": "string",
  • "cap_per": "string",
  • "expiry_days": 30,
  • "trigger": {
    },
  • "action": {
    }
}

Planning

Turn intent into signable transactions.

Parse a natural-language prompt into a structured plan

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.

Authorizations:
PartnerKey
header Parameters
Accept
string
Default: application/json
Enum: "application/json" "text/event-stream"

Set to text/event-stream to receive the plan as Server-Sent Events. Any other value (or omitted) yields a single buffered JSON response.

Request Body schema: application/json
required
prompt
required
string [ 1 .. 2000 ] characters

The desired outcome in plain English.

required
Array of objects (Wallet) non-empty

The wallets in play — at least one.

Array of objects (Balance)

Optional pre-fetched holdings. Omit and Signo fetches them.

object

Descriptive page facts; partner identity and existing scope policy are derived from the authenticated tenant. Not a permissions override.

Array of objects <= 100 items

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.

conversation_context
string

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.

clarify_follow_up
boolean

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.

object

Coarse run options. Raw model ids are never exposed.

Responses

Request samples

Content type
application/json
Example
{
  • "prompt": "move my idle USDC into the best stable yield across my chains",
  • "wallets": [
    ]
}

Response samples

Content type
Example
{
  • "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": [
    ],
  • "clarifications": [ ]
}

Resolve intents into unsigned, ready-to-sign transactions

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
required
Array of objects (Intent) non-empty

The intents[] from /intent (or hand-built to the same shape).

required
Array of objects (Wallet) non-empty

Signing wallets, each ideally carrying the on-chain balances the feasibility gate reads.

Responses

Request samples

Content type
application/json
{
  • "intents": [
    ],
  • "wallets": [
    ]
}

Response samples

Content type
application/json
{
  • "steps": [
    ],
  • "summary": {
    }
}

Execution

Re-quote and simulate steps at sign time.

Re-quote a single step at sign time

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
required
object (Step)

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.

actual_amount
required
number

The real amount, in token units (not wei), to re-encode the step for.

Responses

Request samples

Content type
application/json
{
  • "step": {
    },
  • "actual_amount": 12391.4
}

Response samples

Content type
application/json
Example
{
  • "ok": true,
  • "data": "0x617ba037...updated",
  • "amount_raw": "12391400000",
  • "value": "0"
}

Dry-run steps against a fork before signing

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
chain
required
string
Enum: "ethereum" "base" "arbitrum" "optimism" "polygon" "solana"

The chain the steps execute on — one simulate call per chain. EVM slugs dispatch to the Tenderly fork; solana dispatches to the Solana simulator.

sender
required
string

The address the simulation runs FROM (your user's signing wallet). EVM: required 0x-hex. Solana: applied to any step missing its own wallet.

required
Array of objects (Step) non-empty

The steps[] from /plan to dry-run.

Responses

Request samples

Content type
application/json
{
  • "chain": "arbitrum",
  • "sender": "0x0036534C48a754163F0f446601f36BCfdA840f57",
  • "steps": [
    ]
}

Response samples

Content type
application/json
{
  • "success": true,
  • "gas_used": "246,000",
  • "gas_price": "0.02 gwei",
  • "gas_price_source": "rpc",
  • "total_cost": "$0.02",
  • "error": null,
  • "steps": [
    ]
}

Streaming (SSE)

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.

Multi-turn conversations

/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."

Clarifications

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

Agents

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.

Create an agent — plain-English or typed

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
One of
autonomy
string
Enum: "notify" "prepare"

Defaults to notify on creation; omitted on update preserves existing autonomy.

action
string [ 3 .. 2000 ] characters

Action to prepare at trigger time. Requires autonomy prepare.

idempotency_key
string [ 1 .. 128 ] characters

Tenant-scoped retry key. Same key with a changed body returns 409.

request
required
string [ 3 .. 500 ] characters
wallets
required
Array of strings (AgentWallets) [ 1 .. 10 ] items

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).

Responses

Request samples

Content type
application/json
Example
{
  • "request": "notify me when ETH is above 5000",
  • "wallets": [
    ]
}

Response samples

Content type
application/json
{
  • "status": "needs_clarification",
  • "question": "Which \"gas\" — Ethereum L1 gwei, or a specific chain?",
  • "options": [
    ]
}

List agents

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.

Authorizations:
PartnerKey
query Parameters
status
string
Enum: "active" "paused"

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.

kind
string
Enum: "price" "yield" "health" "aave-health" "spark-health" "portfolio" "news" "condition" "schedule"

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.

wallet
string

Scope to agents whose wallet set includes this address (case-insensitive).

cursor
string

Opaque pagination cursor from a previous response's next_cursor.

limit
integer [ 1 .. 200 ]
Default: 50

Responses

Response samples

Content type
application/json
{
  • "agents": [
    ],
  • "next_cursor": "string"
}

Get one agent

Authorizations:
PartnerKey
path Parameters
id
required
string
Example: w_condition_k17f2a9c

The public agent id, e.g. w_condition_k17f2a9c.

Responses

Response samples

Content type
application/json
{
  • "agent": {
    }
}

Pause/resume an agent, or recompile a condition agent's text

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.

Authorizations:
PartnerKey
path Parameters
id
required
string
Example: w_condition_k17f2a9c

The public agent id, e.g. w_condition_k17f2a9c.

Request Body schema: application/json
required
One of
status
required
string
Enum: "paused" "active"

Responses

Request samples

Content type
application/json
Example
{
  • "status": "paused"
}

Response samples

Content type
application/json
Example
{
  • "agent": {
    }
}

Delete an agent

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.

Authorizations:
PartnerKey
path Parameters
id
required
string
Example: w_condition_k17f2a9c

The public agent id, e.g. w_condition_k17f2a9c.

Responses

Response samples

Content type
application/json
{
  • "error": "invalid_key",
  • "friendly_message": "Missing or malformed API key. Pass it as `Authorization: Bearer sg_...`.",
  • "issues": [ ]
}

Manually trigger an agent once, for integration testing

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).

Authorizations:
PartnerKey
path Parameters
id
required
string
Example: w_condition_k17f2a9c

Responses

Response samples

Content type
application/json
{
  • "value": {
    }
}

List/replay trigger events — the pull rail

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.

Authorizations:
PartnerKey
query Parameters
cursor
string

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.

limit
integer [ 1 .. 200 ]
Default: 50

Responses

Response samples

Content type
application/json
{
  • "events": [
    ],
  • "next_cursor": null
}

Webhook delivery

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).

An agent trigger pushed to your registered endpoint Webhook

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.

Authorizations:
PartnerKey
header Parameters
X-Signo-Event-Id
required
string
Example: evt_k7m2ab91

Identical to the payload's id. The stable dedup key across retries and across both delivery rails.

X-Signo-Signature
required
string
Example: t=1753142400,v1=5f6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e

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:

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);
}
Request Body schema: application/json
required
object (AgentPreparation)

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.

id
required
string
type
required
string
Enum: "agent.triggered" "agent.degraded" "agent.test"
created
required
string <date-time>

ISO-8601. (The pull rail's equivalent field is created_at, an integer epoch-ms, for the same instant.)

required
object
required
object

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.

property name*
additional property
any

Responses

Request samples

Content type
application/json
Example
{
  • "id": "evt_k7m2ab91",
  • "type": "agent.triggered",
  • "created": "2026-07-30T18:40:00.000Z",
  • "agent": {
    },
  • "trigger": {
    }
}

Register a webhook endpoint

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
url
required
string <= 2048 characters

Responses

Request samples

Content type
application/json

Response samples

Content type
application/json
{}

List registered webhook endpoints

Never includes secrets — only the POST response ever carries the raw signing_secret.

Authorizations:
PartnerKey

Responses

Response samples

Content type
application/json
{
  • "endpoints": [
    ]
}

Remove a webhook endpoint

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.

Authorizations:
PartnerKey
path Parameters
id
required
string
Example: k9c7f2ab

Responses

Response samples

Content type
application/json
{
  • "error": "invalid_key",
  • "friendly_message": "Missing or malformed API key. Pass it as `Authorization: Bearer sg_...`.",
  • "issues": [ ]
}

Telegram

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.

Whether a wallet has an active Telegram link

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[].

Authorizations:
PartnerKey
query Parameters
address
required
string
Example: address=0x8f2a00000000000000000000000000000000d5

EVM 0x address (40 hex) or Solana base58 address (32-44 chars).

chain
required
string
Enum: "ethereum" "base" "arbitrum" "optimism" "polygon" "robinhood" "hyperliquid" "solana"
Example: chain=ethereum

Chain slug, matching GET /meta's chains[].

Responses

Response samples

Content type
application/json
Example
{
  • "address": "0x8f2a00000000000000000000000000000000d5",
  • "chain": "ethereum",
  • "linked": true
}

Shield

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.

Read a proposal created by this tenant

Authorizations:
PartnerKey
path Parameters
id
required
string

Responses

Read the permission authorized from this tenant proposal

Authorizations:
PartnerKey
path Parameters
id
required
string

Responses

Read bounded execution runs for this tenant proposal

Authorizations:
PartnerKey
path Parameters
id
required
string
query Parameters
limit
integer [ 1 .. 100 ]
Default: 20

Responses

Partner surfaces

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.

List this partner's in-page agent definitions

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.

Authorizations:
PartnerKey

Responses

Response samples

Content type
application/json
{
  • "schema_version": "1",
  • "agents": [
    ]
}

Resolve browser-safe state for an in-page surface

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
One of
agent
required
string <= 100 characters
required
object (SurfaceWallet)

The wallet whose Telegram link and in-page agent state the surface represents.

required
object or object (SurfaceSubject)
object (SurfaceDisplayContext)

Presentation-only page context. Never used for identity, authorization, arming, or pricing.

include
Array of strings <= 1 items
Items Value: "result"

Optionally include the latest structured prepared result when one exists.

Responses

Request samples

Content type
application/json
Example
{
  • "agent": "pendle-fixed-entry",
  • "wallet": {
    },
  • "subject": {
    },
  • "display_context": {
    }
}

Response samples

Content type
application/json
Example
{
  • "intent": "agent",
  • "wallet": {
    },
  • "linked": true,
  • "definition": {
    },
  • "subject": {
    },
  • "display_context": {
    },
  • "existing": {
    },
  • "result": {
    }
}

Validate submitted values and mint an opaque activation URL

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
One of
agent
required
string <= 100 characters
schema_version
required
string
Value: "1"
required
object (SurfaceWallet)

The wallet whose Telegram link and in-page agent state the surface represents.

required
object or object (SurfaceSubject)
object (SurfaceDisplayContext)

Presentation-only page context. Never used for identity, authorization, arming, or pricing.

object

User values keyed by ids in the resolved definition's fields array.

action_id
string

Stable id from the resolved definition's actions array.

Responses

Request samples

Content type
application/json
Example
{
  • "agent": "pendle-fixed-entry",
  • "schema_version": "1",
  • "wallet": {
    },
  • "subject": {
    },
  • "field_values": {
    },
  • "action_id": "propose-entry"
}

Response samples

Content type
application/json
Example
{
  • "outcome": "SurfaceActivatedResponse",
  • "intent": "agent",
  • "agent": {
    },
  • "change": "created",
  • "rendered": "string"
}

Exchange a prepared result for a short-lived review URL

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.

Authorizations:
PartnerKey
Request Body schema: application/json
required
schema_version
required
string
Value: "1"
agent
required
string <= 100 characters
required
object (SurfaceWallet)

The wallet whose Telegram link and in-page agent state the surface represents.

required
object or object (SurfaceSubject)
result_id
required
string <= 100 characters

Opaque id returned in the resolved result envelope.

Responses

Request samples

Content type
application/json
{
  • "schema_version": "1",
  • "agent": "pendle-fixed-entry",
  • "wallet": {
    },
  • "subject": {
    },
  • "result_id": "notification-1"
}

Response samples

Content type
application/json
{}

Positions

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.

A wallet's addressable positions, with canonical ids

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.

Authorizations:
PartnerKey
query Parameters
wallet
required
string
Example: wallet=0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82

EVM 0x address (40 hex). Pendle and Saffron positions are EVM-only today.

protocol
string
Enum: "pendle" "saffron"
Example: protocol=pendle

Filter to one protocol with addressable positions.

kind
string
Enum: "pt" "yt" "lp" "fixed" "variable"
Example: kind=pt

Filter to one position kind. Pendle emits pt, yt, lp; Saffron emits fixed, variable.

chain
string
Enum: "ethereum" "base" "arbitrum" "optimism" "polygon" "robinhood" "hyperliquid" "solana"
Example: chain=ethereum

Filter to one chain slug, matching GET /meta's chains[].

Responses

Response samples

Content type
application/json
Example
{
  • "wallet": "0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82",
  • "positions": [
    ],
  • "degraded": false,
  • "unavailable": [ ]
}

Account

Usage, credit balance, and capability discovery.

Return the tenant's credit balance and recent ledger

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.

Authorizations:
PartnerKey

Responses

Response samples

Content type
application/json
{
  • "balance": 4820,
  • "cost_per_call": 5,
  • "recent": [
    ]
}

Discover schema version, supported actions, chains, and limits

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.

Authorizations:
PartnerKey

Responses

Response samples

Content type
application/json
{
  • "schema_version": "2026-09-08",
  • "cost_per_call": 5,
  • "actions": [
    ],
  • "chains": [
    ],
  • "rate_tiers": {
    }
}

Versioning

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.