Download OpenAPI specification:Download
Turn a plain-English outcome into a ready-to-sign, multi-step, cross-chain transaction plan.
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.
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.
POST /intent — natural language in, a structured plan out (the intents[]:
what to do, not yet how). May return clarifications instead when the
request is ambiguous.POST /plan — the intents[] in, unsigned transactions out (the steps[]:
real calldata your signer executes).POST /steps/reencode — re-quote a single step at sign time. Required for
correct integrations: chained and max amounts are re-sized from live
balances, and expiring routes (Jupiter blockhash, LiFi tool pinning) are
refreshed. Returns 409 when a route is gone and the step must be re-planned.POST /simulate (optional) — dry-run the steps[] against a fork before
signing to surface reverts and gas up front.Every request is authenticated with a partner API key as a bearer token
(Authorization: Bearer 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.
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.
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.
| 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 |
{- "wallet": "string",
- "request": "stringst",
- "autonomy": "notify"
}{- "proposal_id": "string",
- "expires_at": 0,
- "status": "draft",
- "wallet": "string",
- "summary": "string",
- "cadence": "hourly",
- "autonomy": "notify",
- "action": "string",
- "activation": "string"
}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.
| 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 |
{- "schema_version": 2,
- "wallet": "string",
- "chain_id": 196,
- "budget": "string",
- "cap_per": "string",
- "expiry_days": 30,
- "trigger": {
- "kind": "health_factor",
- "comparator": "<",
- "value": 100
}, - "action": {
- "kind": "transfer",
- "asset": "string",
- "recipient": "string"
}
}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.
| Accept | string Default: application/json Enum: "application/json" "text/event-stream" Set to |
| prompt required | string [ 1 .. 2000 ] characters The desired outcome in plain English. |
required | Array of objects (Wallet) non-empty The wallets in play — at least one. |
Array of objects (Balance) Optional pre-fetched holdings. Omit and 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 | |
| conversation_context | string Legacy descriptive context. Addresses here do not count as typed by
the user; paste the address in the current Prior turns of the conversation, for multi-turn planning: the prior turns
newline-joined, one turn per line, each line 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 |
object Coarse run options. Raw model ids are never exposed. |
{- "prompt": "move my idle USDC into the best stable yield across my chains",
- "wallets": [
- {
- "address": "0x8f2a...d5",
- "chain": "ethereum"
}
]
}{- "reasoning": "You hold 12,400 USDC idle on Ethereum. Aave v3 on Base is paying the best stable rate right now, so I'll bridge to Base and deposit.",
- "suggested_name": "Idle USDC → best stable yield",
- "kind": "transactional",
- "intents": [
- {
- "action": "bridge",
- "asset": "USDC",
- "from_chain": "ethereum",
- "to_chain": "base",
- "amount": "max"
}, - {
- "action": "deposit",
- "protocol": "aave",
- "asset": "USDC",
- "chain": "base",
- "amount": "chained",
- "pool_id": "aave-v3-base-usdc",
- "vault_address": "0xa238dd80..."
}
], - "clarifications": [ ]
}Wraps the app's /api/intent-plan resolver — trusted-vault gating, Pendle-PT
expansion, route prefetch, feasibility checks, and the repair pass are all
inherited. Takes the intents[] from /intent (or hand-built to the same
shape) plus the signing wallets[] with their on-chain balances, and returns
the ordered steps[] of unsigned transactions. Costs 5 credits per call.
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.
required | Array of objects (Intent) non-empty The |
required | Array of objects (Wallet) non-empty Signing wallets, each ideally carrying the on-chain |
{- "intents": [
- {
- "action": "transfer",
- "asset": "USDC",
- "chain": "base",
- "from_chain": "string",
- "to_chain": "string",
- "from_asset": "string",
- "to_asset": "string",
- "from": "string",
- "to": "string",
- "spender": "string",
- "protocol": "string",
- "pool_id": "string",
- "vault_address": "string",
- "amount": "max",
- "min_amount": 0,
- "slippage_bps": 1,
- "sources": [
- {
- "chain": "string",
- "wallet": "string"
}
], - "destination": {
- "chain": "string",
- "wallet": "string"
}
}
], - "wallets": [
- {
- "address": "0x8f2a...d5",
- "chain": "ethereum",
- "label": "string",
- "balances": [
- {
- "symbol": "USDC",
- "amount": 12400,
- "usd_value": 0,
- "chain": "string"
}
]
}
]
}{- "steps": [
- {
- "order": 1,
- "type": "bridge",
- "chain": "ethereum",
- "protocol": "LiFi",
- "action_type": "Bridge",
- "asset": "USDC",
- "amount": 12400,
- "to": "0x1231deb6...",
- "data": "0x4630a0d8...",
- "value": "0",
- "description": "Bridge 12,400 USDC Ethereum → Base via LiFi",
- "wait_for_bridge": true
}, - {
- "order": 2,
- "type": "deposit",
- "chain": "base",
- "protocol": "aave",
- "action_type": "Deposit",
- "asset": "USDC",
- "amount": 0,
- "to": "0xa238dd80...",
- "data": "0x617ba037...",
- "value": "0",
- "description": "Supply USDC to Aave v3 on Base",
- "chained": {
- "from_key": "0x8f2a...d5@base@USDC",
- "estimate_amount": 12391.4,
- "min_output": 12300
}
}
], - "summary": {
- "total_after_fees": "12,391.4 USDC",
- "bridge_count": 1,
- "est_fee_usd": "3.10"
}
}Wraps the app's /api/re-encode-step. Re-encodes one step against the
real amount available at sign time — required for correct integrations:
chained and max amounts are re-sized from live balances, and expiring
routes (Jupiter blockhash, LiFi tool pinning) are refreshed so the user signs
the same aggregator and fee structure they were quoted. Returns 409 with
error: route_expired when a pinned route is gone and the step must be
re-planned via /plan.
Errors: 503 engine_unreachable when the encoding service cannot be reached.
Retryable, and no credits are charged.
required | object (Step) One unsigned transaction. EVM steps carry |
| actual_amount required | number The real amount, in token units (not wei), to re-encode the step for. |
{- "step": {
- "type": "deposit",
- "asset": "USDC",
- "chain": "base",
- "protocol": "aave",
- "to": "0xa238dd80...",
- "data": "0x617ba037..."
}, - "actual_amount": 12391.4
}{- "ok": true,
- "data": "0x617ba037...updated",
- "amount_raw": "12391400000",
- "value": "0"
}Wraps the app's simulation layer (Tenderly for EVM, a Solana simulate for
Solana). Executes the steps[] against a fork and returns per-step success,
status, and gas so you can surface reverts and cost before anything is signed.
Read-only — nothing is broadcast. Costs 5 credits per call.
Calldata-bearing steps are pre-validated against the target address: a step that CALLS an address with no deployed code (dead or self-destructed contract) returns 400 before any simulation runs. Plain value transfers to EOAs are exempt and simulate normally.
Errors: 503 engine_unreachable when the encoding service cannot be reached.
Retryable, and no credits are charged.
| chain required | string Enum: "ethereum" "base" "arbitrum" "optimism" "polygon" "solana" The chain the steps execute on — one simulate call per chain. EVM slugs dispatch to the Tenderly fork; |
| sender required | string The address the simulation runs FROM (your user's signing wallet). EVM: required 0x-hex. Solana: applied to any step missing its own |
required | Array of objects (Step) non-empty The |
{- "chain": "arbitrum",
- "sender": "0x0036534C48a754163F0f446601f36BCfdA840f57",
- "steps": [
- {
- "order": 0,
- "type": "transfer",
- "chain": "string",
- "chain_id": 0,
- "protocol": "string",
- "action_type": "string",
- "asset": "string",
- "from_chain": "string",
- "to_chain": "string",
- "amount": 0,
- "amount_raw": "string",
- "to": "string",
- "data": "string",
- "value": "string",
- "description": "string",
- "estimated_gas": "string",
- "bridge_fee": 0,
- "bridge_time": "string",
- "wait_for_bridge": true,
- "chained": {
- "from_key": "string",
- "estimate_amount": 0,
- "min_output": 0
}, - "pool_id": "string",
- "solana_tx": "string",
- "solana_format": "legacy",
- "solana_error": "string",
- "slippage": 0,
- "fee_bps": 0,
- "swap_tool": "string",
- "error": "string",
- "success": true
}
]
}{- "success": true,
- "gas_used": "246,000",
- "gas_price": "0.02 gwei",
- "gas_price_source": "rpc",
- "total_cost": "$0.02",
- "error": null,
- "steps": [
- {
- "success": true,
- "status": "✅",
- "status_code": 200,
- "action": "Approve USDC for Kyberswap",
- "action_type": "Approve",
- "asset": "USDC",
- "gas": "36,138",
- "error": null
}, - {
- "success": true,
- "status": "✅",
- "status_code": 200,
- "action": "Swap 1 USDC for ETH",
- "action_type": "Swap",
- "asset": "USDC",
- "gas": "209,862",
- "error": null
}
]
}Send Accept: text/event-stream to /intent (or options.stream = true) to stream
the plan as it is built — the same event set the app renders: delta and reasoning
(prose as it is written), tool-start / tool-end (render "thinking" status lines),
and a terminal final event carrying the structured intents / clarifications.
Omit the header for a single buffered JSON response.
/intent is stateless. 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."
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
Programmatic alerts (price / yield / health-factor / portfolio-move / schedule / a plain-English condition) scoped to your tenant. Not metered by credits — gated by a per-tenant active-agent quota (by rate tier) and the standard per-key rate limit.
Send either a plain-English body { request, wallets } or a typed body
{ kind, wallets, ... } — never both, never neither (400 otherwise). Not
metered by credits; gated instead by a per-tenant active-agent quota (429 agent_quota_exceeded once the tier cap is hit — beta 50 / standard 500 /
high 5000).
Plain-English ({ request, wallets }) runs the same compile pipeline the
Signo app and Telegram bot use. Three outcomes:
201 with agent + compile.201,
created as a kind: schedule agent.200 with status: needs_clarification, a question, and up to
4 options. Re-submit by sending request again with the picked option
folded in: "<original request>\n\nClarification — <question>: <picked label>" — the same inline convention /intent's clarification round-trip
uses, just folded into the next request string (there's no multi-turn state
to carry here).422 unwatchable.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.
| 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 |
{- "request": "notify me when ETH is above 5000",
- "wallets": [
- "0x8f2a...d5"
]
}{- "status": "needs_clarification",
- "question": "Which \"gas\" — Ethereum L1 gwei, or a specific chain?",
- "options": [
- {
- "id": "opt_0",
- "label": "Ethereum L1 gas (gwei)"
}, - {
- "id": "opt_1",
- "label": "Base gas (gwei)"
}
]
}Tenant-scoped, newest-first, offset-paginated. Both status and kind filter
against the public vocabulary shown on the returned agent objects (not
the internal per-table storage) — see the callouts on each parameter below.
| status | string Enum: "active" "paused" Filters on the public status (see the |
| kind | string Enum: "price" "yield" "health" "aave-health" "spark-health" "portfolio" "news" "condition" "schedule"
|
| wallet | string Scope to agents whose wallet set includes this address (case-insensitive). |
| cursor | string Opaque pagination cursor from a previous response's |
| limit | integer [ 1 .. 200 ] Default: 50 |
{- "agents": [
- {
- "autonomy": "notify",
- "action": "string",
- "id": "w_condition_k17f2a9c",
- "type": "alert",
- "kind": "price",
- "status": "active",
- "created_at": 0,
- "last_checked_at": 0,
- "last_fired_at": 0,
- "wallets": [
- "0x8f2a...d5"
], - "condition": "above",
- "threshold": 0,
- "asset_symbol": "string",
- "coingecko_id": "string",
- "pct_change": 0,
- "base_price": 0,
- "pool_id": "string",
- "protocol": "string",
- "chain": "string",
- "asset": "string",
- "metric": "apy",
- "target_wallet": "string",
- "movement_window": "24h",
- "rendered": "string",
- "fire_headline": "string",
- "cadence": "hourly",
- "intent": "string",
- "label": "string",
- "hour": 0,
- "minute": 0,
- "day_of_week": 0,
- "day_of_month": 0,
- "timezone": "string"
}
], - "next_cursor": "string"
}| id required | string Example: w_condition_k17f2a9c The public agent id, e.g. |
{- "agent": {
- "autonomy": "notify",
- "action": "string",
- "id": "w_condition_k17f2a9c",
- "type": "alert",
- "kind": "price",
- "status": "active",
- "created_at": 0,
- "last_checked_at": 0,
- "last_fired_at": 0,
- "wallets": [
- "0x8f2a...d5"
], - "condition": "above",
- "threshold": 0,
- "asset_symbol": "string",
- "coingecko_id": "string",
- "pct_change": 0,
- "base_price": 0,
- "pool_id": "string",
- "protocol": "string",
- "chain": "string",
- "asset": "string",
- "metric": "apy",
- "target_wallet": "string",
- "movement_window": "24h",
- "rendered": "string",
- "fire_headline": "string",
- "cadence": "hourly",
- "intent": "string",
- "label": "string",
- "hour": 0,
- "minute": 0,
- "day_of_week": 0,
- "day_of_month": 0,
- "timezone": "string"
}
}Send either { status } or { request }.
{ status: "paused" | "active" } works identically on any agent kind — the
public vocabulary is always active/paused regardless of what's stored
internally per kind (see the Agent.status schema note). An id that's
well-formed but doesn't resolve to a row you own (already deleted, or another
tenant's) is 404, not 500.
{ request: "<new plain-English text>" } only works on kind: "condition"
agents (400 on any other kind — delete and recreate a typed agent
instead). 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.
| id required | string Example: w_condition_k17f2a9c The public agent id, e.g. |
| status required | string Enum: "paused" "active" |
{- "status": "paused"
}{- "agent": {
- "autonomy": "notify",
- "action": "string",
- "id": "w_condition_k17f2a9c",
- "type": "alert",
- "kind": "price",
- "status": "active",
- "created_at": 0,
- "last_checked_at": 0,
- "last_fired_at": 0,
- "wallets": [
- "0x8f2a...d5"
], - "condition": "above",
- "threshold": 0,
- "asset_symbol": "string",
- "coingecko_id": "string",
- "pct_change": 0,
- "base_price": 0,
- "pool_id": "string",
- "protocol": "string",
- "chain": "string",
- "asset": "string",
- "metric": "apy",
- "target_wallet": "string",
- "movement_window": "24h",
- "rendered": "string",
- "fire_headline": "string",
- "cadence": "hourly",
- "intent": "string",
- "label": "string",
- "hour": 0,
- "minute": 0,
- "day_of_week": 0,
- "day_of_month": 0,
- "timezone": "string"
}
}A structurally-invalid id (doesn't match the w_<type>_<id> shape) is 404.
Idempotent: a well-formed id whose row is already gone (e.g. a repeated
delete, or a retry after a timed-out first attempt) returns 204 as a no-op —
it never distinguishes "already gone" from "just deleted" in the response.
| id required | string Example: w_condition_k17f2a9c The public agent id, e.g. |
{- "error": "invalid_key",
- "friendly_message": "Missing or malformed API key. Pass it as `Authorization: Bearer sg_...`.",
- "issues": [ ]
}Queues a synthetic trigger through the exact same delivery pipeline a real trigger
takes (webhook push + the GET /agent-events pull rail both see it),
flagged type: "agent.test" — independent of whether the agent's real
trigger is currently met. 202 because the event is queued into the same
delivery sweep a real trigger uses, not returned inline; it typically lands
within the next delivery-cron tick (every 2 minutes).
| id required | string Example: w_condition_k17f2a9c |
{- "value": {
- "ok": true,
- "note": "test event will be delivered like a real trigger, flagged test:true"
}
}Tenant-scoped trigger history, newest-first, cursor-paginated on triggered_at.
This is how a partner without a registered webhook endpoint reconciles triggers,
and how any partner replays or backfills around a delivery gap. Delivery
(both this rail and the webhook push rail below) is at-least-once — dedup
on id regardless of which rail an event was read from.
Note: this rail and the pushed webhook body (see the webhooks section)
are built from the same event mapping — id, type, agent, and the set of
trigger fields are identical for the same trigger on both rails. The one
intentional difference is the timestamp: this rail returns created_at as an
integer epoch-ms, while the pushed webhook body returns created as an
ISO-8601 string, for the same instant. Both rails' trigger objects are
snake_case (e.g. value_at_trigger), so a handler can parse either
rail's event with the same code.
| cursor | string Opaque pagination cursor from a previous response's |
| limit | integer [ 1 .. 200 ] Default: 50 |
{- "events": [
- {
- "id": "evt_k7m2ab91",
- "type": "agent.triggered",
- "created_at": 1753142400000,
- "agent": {
- "id": "k17f2a9c",
- "kind": "condition"
}, - "trigger": {
- "headline": "ETH crossed above $5,000",
- "kind": "condition",
- "condition": "above",
- "threshold": 0,
- "wallet": "0x8f2a...d5"
}
}
], - "next_cursor": null
}Register an HTTPS endpoint that agent triggers are pushed to, and pull/replay trigger
history. See the webhooks section below for the delivery contract (signing,
retries, dead-lettering).
Sent to every active endpoint registered for your tenant whenever 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:
created (an ISO-8601 string); the pull rail's
equivalent is created_at (an integer epoch-ms) — same instant, different
field name and format.snake_case keys inside trigger (e.g. value_at_trigger),
matching every other public object in this API.In both rails, agent.id is the SAME public w_<type>_<id> value returned
by create/list/get — pass it straight back to /agents/{id},
/agents/{id}/test, PATCH or DELETE without rebuilding it. Note the
prefix is the storage type (alert | condition | reminder), NOT the public
kind: a kind: "price" agent is w_alert_..., so kind alone is not
enough to construct the id — which is why the event carries it. It is identical
across both rails for the same trigger, including manual test-triggers
(POST /agents/{id}/test) of every kind.
| X-Signo-Event-Id required | string Example: evt_k7m2ab91 Identical to the payload's |
| X-Signo-Signature required | string Example: t=1753142400,v1=5f6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e
|
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 |
required | object |
required | object Same field set as |
| property name* additional property | any |
{- "id": "evt_k7m2ab91",
- "type": "agent.triggered",
- "created": "2026-07-30T18:40:00.000Z",
- "agent": {
- "id": "k17f2a9c",
- "kind": "condition"
}, - "trigger": {
- "headline": "ETH crossed above $5,000",
- "kind": "condition",
- "condition": "above",
- "threshold": 0,
- "wallet": "0x8f2a...d5"
}
}https:// only, 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.
| url required | string <= 2048 characters |
{
}{- "signing_secret": "whs_3f9a7b2e1c4d8f6a0b5e9c2d7a1f4b8e6c0d3a9f7b2e5c1d8a4f0b6e3c9d7a2f"
}Never includes secrets — only the POST response ever carries the raw signing_secret.
{- "endpoints": [
- {
- "id": "string",
- "url": "string",
- "status": "active",
- "disabled_reason": "string",
- "created_at": 0
}
]
}Idempotent: an id that doesn't resolve to a row you own (already removed, or
another tenant's) still returns 204 as a no-op.
| id required | string Example: k9c7f2ab |
{- "error": "invalid_key",
- "friendly_message": "Missing or malformed API key. Pass it as `Authorization: Bearer sg_...`.",
- "issues": [ ]
}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.
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[].
| address required | string Example: address=0x8f2a00000000000000000000000000000000d5 EVM |
| chain required | string Enum: "ethereum" "base" "arbitrum" "optimism" "polygon" "robinhood" "hyperliquid" "solana" Example: chain=ethereum Chain slug, matching |
{- "address": "0x8f2a00000000000000000000000000000000d5",
- "chain": "ethereum",
- "linked": true
}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.
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.
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.
{- "schema_version": "1",
- "agents": [
- {
- "agent": "string",
- "schema_version": "1",
- "title": "string",
- "description": "string",
- "subject": {
- "kind": "market",
- "required": [
- "id"
]
}, - "capabilities": [
- "notify"
], - "fields": [
- {
- "id": "string",
- "type": "number",
- "label": "string",
- "required": true,
- "default_value": "string",
- "options": [
- {
- "value": "string",
- "label": "string"
}
], - "suggestions": [
- "string"
], - "step": 0,
- "suffix": "string"
}
], - "actions": [
- {
- "id": "string",
- "label": "string",
- "capability": "notify"
}
], - "copy": {
- "trigger": "string",
- "submit": "string"
}
}
]
}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.
| 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. |
{- "agent": "pendle-fixed-entry",
- "wallet": {
- "address": "0x8f2a00000000000000000000000000000000d5",
- "chain": "ethereum"
}, - "subject": {
- "kind": "market",
- "id": "0x4f3100000000000000000000000000000000ab12",
- "chain": "ethereum"
}, - "display_context": {
- "title": "PT-sUSDe"
}
}{- "intent": "agent",
- "wallet": {
- "address": "string",
- "chain": "string"
}, - "linked": true,
- "definition": {
- "agent": "string",
- "schema_version": "1",
- "title": "string",
- "description": "string",
- "subject": {
- "kind": "market",
- "required": [
- "id"
]
}, - "capabilities": [
- "notify"
], - "fields": [
- {
- "id": "string",
- "type": "number",
- "label": "string",
- "required": true,
- "default_value": "string",
- "options": [
- {
- "value": "string",
- "label": "string"
}
], - "suggestions": [
- "string"
], - "step": 0,
- "suffix": "string"
}
], - "actions": [
- {
- "id": "string",
- "label": "string",
- "capability": "notify"
}
], - "copy": {
- "trigger": "string",
- "submit": "string"
}
}, - "subject": {
- "kind": "market",
- "id": "0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82|pendle|0x3f5ea53d1160177445b1898afbb16da111182418|pt",
- "chain": "string"
}, - "display_context": {
- "title": "string",
- "subtitle": "string",
- "metadata": {
- "property1": "string",
- "property2": "string"
}
}, - "existing": {
- "rendered": "string",
- "id": "string",
- "status": "active",
- "field_values": {
- "property1": "string",
- "property2": "string"
}, - "action_id": "string",
- "activated_at": 0
}, - "result": {
- "schema_version": "1",
- "id": "string",
- "status": "prepared",
- "summary": "string",
- "rationale": "string",
- "prepared_at": 0,
- "expires_at": 0,
- "plan_preview": {
- "steps": [
- {
- "action": "string",
- "label": "string",
- "details": [
- {
- "type": "rate",
- "label": "string",
- "value": 0,
- "unit": "percent"
}
]
}
]
}, - "review": {
- "available": true
}
}
}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.
| 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 | |
| action_id | string Stable id from the resolved definition's |
{- "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"
}{- "outcome": "SurfaceActivatedResponse",
- "intent": "agent",
- "agent": {
- "rendered": "string",
- "id": "string",
- "status": "active",
- "field_values": {
- "property1": "string",
- "property2": "string"
}, - "action_id": "string",
- "activated_at": 0
}, - "change": "created",
- "rendered": "string"
}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.
| 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 |
{- "schema_version": "1",
- "agent": "pendle-fixed-entry",
- "wallet": {
- "address": "0x8f2a00000000000000000000000000000000d5",
- "chain": "ethereum"
}, - "subject": {
- "kind": "market",
- "id": "0x4f3100000000000000000000000000000000ab12",
- "chain": "ethereum"
}, - "result_id": "notification-1"
}{- "expires_at": 0
}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.
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.
| wallet required | string Example: wallet=0x2be4d36fae51fd3b82e3bfcf635859b23ce99c82 EVM |
| 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 |
| chain | string Enum: "ethereum" "base" "arbitrum" "optimism" "polygon" "robinhood" "hyperliquid" "solana" Example: chain=ethereum Filter to one chain slug, matching |
{- "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": [ ]
}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.
{- "balance": 4820,
- "cost_per_call": 5,
- "recent": [
- {
- "kind": "spend",
- "amount": 5,
- "reason": "api_intent",
- "at": 1751909204000
}
]
}Machine-readable capability discovery: the current schema version, the flat per-call credit cost, the supported intent actions, the supported chains, and the rate-limit tiers. Feature-detect from this rather than hard-coding. Free.
{- "schema_version": "2026-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
}
}
}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.