Agents
Agents ship as part of the /v1beta API — same posture as the rest of it: keys come
from the Signo Partner Portal (one per partner during
beta, email the Signo integration team at contact@signo.fi for more), and the surface may
evolve behind the Signo-Schema-Version header until GA.
Programmatic alerts: price crosses, yield/health-factor moves, portfolio swings, a recurring check-in, or a freeform plain-English condition — created over the API, delivered to a webhook endpoint you register (or pulled on demand). Agents default to Notify: an agent tells you something happened and never signs or executes anything on its own. A Prepare agent also stores an action and, when it fires, hands you an unsigned plan to review and sign — see Prepare agents. Automatic execution exists only through a signed Shield permission.
- Base URL:
https://app.signo.fi/api/v1beta(same as the rest of the API) - Auth:
Authorization: Bearer sg_beta_..., scopeagents - Metering: not charged against your credit balance — gated instead by a per-tenant active-agent quota (beta 50 / standard 500 / high 5000, by rate tier) and the usual per-key rate limit
- Reference: every endpoint, schema, and error code is in the API Reference (generated from the OpenAPI spec) — this page is the narrative walkthrough
Delivery: recommended vs. optional
An agent triggers whether or not you've registered anything to receive it. There are two ways to see its triggers:
- Recommended — register a webhook endpoint.
POST /webhook-endpointsonce, and every trigger is signed and pushed to it automatically. No polling. - Optional alternative — poll
GET /agent-events. Skip registration entirely and read triggers on your own schedule instead.
An agent created with no endpoint registered still arms, evaluates, and triggers exactly the same — it just has nowhere to push to, so nothing shows up unless you poll. If you're building an always-on integration, register an endpoint first; use the pull rail for a quick test, a backfill, or a partner that genuinely prefers polling.
Quickstart
Three calls: register where triggers get delivered, create an agent in plain English, then trigger a test trigger so you can see the whole loop without waiting for a real market move.
1. Register a webhook endpoint (recommended).
curl https://app.signo.fi/api/v1beta/webhook-endpoints \
-H "Authorization: Bearer sg_beta_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/webhooks/flip"}'
{
"endpoint": { "id": "k9c7f2ab", "url": "https://example.com/webhooks/flip" },
"signing_secret": "whs_3f9a7b2e1c4d8f6a0b5e9c2d7a1f4b8e6c0d3a9f7b2e5c1d8a4f0b6e3c9d7a2f"
}
Store signing_secret now — it's returned exactly once and can't be fetched again
(only its hash is kept server-side). You'll use it to verify inbound deliveries; see
Verifying signatures below. Up to 5 endpoints per tenant.
Skipping this step is fine too — it just means step 3 below won't push anywhere until
you poll GET /agent-events instead.
2. Create an agent.
curl https://app.signo.fi/api/v1beta/agents \
-H "Authorization: Bearer sg_beta_..." \
-H "Content-Type: application/json" \
-d '{"request":"notify me when ETH is above 5000","wallets":["0x8f2a...d5"]}'
{
"agent": {
"id": "w_condition_k17f2a9c",
"type": "condition",
"kind": "condition",
"status": "active",
"created_at": 1753142400000,
"wallets": ["0x8f2a...d5"],
"intent": "ETH is above 5000",
"rendered": "ETH price is above $5,000",
"cadence": "hourly"
},
"compile": { "rendered": "ETH price is above $5,000", "fire_headline": null, "cadence": "hourly" }
}
Same compile pipeline the Signo app and Telegram bot run on your text — if it's ambiguous you get a clarification instead of an agent; see The clarification round-trip.
intent is the trigger the compiler extracted, not a verbatim echo of what you sent —
"notify me when ETH is above 5000" is stored as "ETH is above 5000". You already hold
the original request; what you can't reconstruct is our interpretation, which is what
intent and rendered give you.
Agent ids
Every response that contains an agent — create, list, get, and the agent.id inside
a trigger event — returns the same public id, shaped w_{type}_{id}. Store it and pass
it back verbatim on /agents/{id}, /agents/{id}/test, PATCH and DELETE.
Two things worth knowing so you don't try to build one yourself:
- The prefix is the storage type —
alert,condition, orreminder— not the publickind. Akind: "price"agent has aw_condition_…id. There is no reliable way to derive the id fromkind, which is exactly why events carry it for you. - A bare id without the prefix is not valid on path endpoints and returns
404 agent_not_found.
Which wallets can I watch?
Any address you can write down. Notify and Prepare agents are read-only at fire time — they observe prices, yields, health factors and balances and emit an event (a Prepare agent's plan is unsigned until the owner signs it) — so there is no signature, no connected session, and no ownership check involved. A test wallet, a wallet your user gave you, or an address that has never touched Signo all work identically.
That also means the check is yours: the key authorizes your backend for your tenant, not any particular end user. Management operations apply to agents your key is authorized to manage, not every row originally created with that key. An agent with owner-only Execute authority is excluded; see Pausing, resuming, deleting. Authenticate that the user in your session controls the wallet before you create, change or delete an agent on their behalf.
wallets[0] is the agent's owner for bookkeeping; the array preserves the case you
send (Solana is case-sensitive).
3. Trigger a test trigger to see a real delivery immediately, instead of waiting for
ETH to actually cross $5,000. It returns 202 Accepted: the test event is written and a
delivery attempt starts before the response returns, not on a later tick. The 202 proves
the attempt; the message or webhook arriving proves delivery. A test never proves the
condition was actually met (it fires whether or not it holds, and says so in the message):
curl -X POST https://app.signo.fi/api/v1beta/agents/w_condition_k17f2a9c/test \
-H "Authorization: Bearer sg_beta_..."
Within the next delivery tick (every ~2 minutes) your endpoint receives:
POST /webhooks/flip HTTP/1.1
Content-Type: application/json
X-Signo-Event-Id: evt_k7m2ab91
X-Signo-Signature: t=1753142400,v1=5f6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e3c9d7a2f0b6e
{
"id": "evt_k7m2ab91",
"type": "agent.test",
"created": "2026-07-30T18:41:10.000Z",
"agent": { "id": "w_condition_k17f2a9c", "kind": "condition" },
"trigger": { "headline": "🔔 ETH price is above $5,000", "kind": "condition", "condition": "above", "threshold": 0, "wallet": "0x8f2a...d5" }
}
Return any 2xx within 10 seconds. That's the full loop — a real trigger looks
identical except type: "agent.triggered" and, for kinds other than condition, real
trigger data (value_at_trigger / price_at_trigger / change_pct) alongside trigger.
agent.id is the same public w_{type}_{id} value create and list return, so you
can pause or delete the agent straight from a trigger without a lookup table. Note the
prefix is the storage type (alert | condition | reminder), not the kind —
a kind: "price" agent is w_condition_…. Always pass the id back verbatim rather than
rebuilding it from kind.
Verifying signatures
Every delivery carries X-Signo-Signature: t=<unix_seconds>,v1=<hex hmac> — the Stripe
signed-webhooks convention: hmac_sha256(signing_secret, "<t>.<raw_body>"). Verify
against the raw, unparsed request body (not a re-serialized JSON.stringify of the
parsed object — whitespace differences will break the signature), with a constant-time
comparison, and reject stale timestamps. This mirrors the reference implementation
Signo itself uses to sign (verifySignature in lib/webhooks/sign.ts):
const crypto = require("crypto");
function verifyFlipSignature(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((kv) => {
const i = kv.indexOf("=");
return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
}),
);
const { t, v1 } = parts;
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const expectedBuf = Buffer.from(expected, "hex");
const gotBuf = Buffer.from(v1, "hex");
if (expectedBuf.length !== gotBuf.length) return false;
return crypto.timingSafeEqual(expectedBuf, gotBuf);
}
// Express example — must read the raw body before any JSON-parsing middleware
app.post("/webhooks/flip", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyFlipSignature(process.env.SIGNO_WEBHOOK_SECRET, req.header("X-Signo-Signature"), req.body.toString("utf8"));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body);
// ... handle event.type, dedup on event.id ...
res.status(200).end();
});
Delivery: retries, dead-lettering, dedup
Delivery is at-least-once. Dedup on the payload's id (same value as the
X-Signo-Event-Id header) — never assume a trigger arrives exactly once.
Acknowledge with any 2xx within 10 seconds. Anything else — non-2xx, a timeout, or a
redirect (redirects are never followed) — is a failed attempt, retried with backoff:
| Attempt | Waits before next try |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
After attempt 5 fails, that delivery is dead-lettered — no further retries for that
specific trigger (it's still visible via GET /agent-events, the pull rail, described
next). If an endpoint racks up 10 consecutive dead-lettered deliveries, it's
auto-disabled (GET /webhook-endpoints shows status: "disabled") — fix your
receiver and re-register, or flip it back on, to resume.
The pull rail
No endpoint registered, or catching up after an outage? GET /agent-events returns
the same triggers, cursor-paginated, newest-first:
curl "https://app.signo.fi/api/v1beta/agent-events?limit=50" \
-H "Authorization: Bearer sg_beta_..."
{
"events": [
{
"id": "evt_k7m2ab91",
"type": "agent.triggered",
"created_at": 1753142400000,
"agent": { "id": "w_condition_k17f2a9c", "kind": "condition" },
"trigger": { "headline": "ETH crossed above $5,000", "kind": "condition", "condition": "above", "threshold": 0, "wallet": "0x8f2a...d5" }
}
],
"next_cursor": null
}
The id is the same dedup key either rail hands you, and agent/trigger are built
from the same event mapping the pushed webhook uses — same fields, same values, for
the same trigger. Two differences to know about:
- Timestamp. This rail returns
created_atas an integer epoch-ms; the pushed webhook's equivalent field iscreated, an ISO-8601 string — same instant, just a different field name and format. - Key casing is the same on both rails.
triggerissnake_case(value_at_trigger,price_at_trigger,change_pct) whether the event arrived by push or was pulled, so one parser handles both.
condition and threshold are present on every trigger (both rails) even for kinds
where they're not meaningful — condition- and schedule-kind triggers always show the
placeholder "above" / 0. Trust them for price/yield/health/portfolio kinds.
The clarification round-trip
A plain-English request that's too broad to compile into one measurable check
returns 200 instead of 201:
{
"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)" }
]
}
Three things to know before you build against this:
- It's a
200, and so is nothing else about it. A clarification and a successful create are both 2xx. Branch on the body, not onres.ok— check forstatus === "needs_clarification"before you go looking foragent.id. An agent that only checks the status class will silently treat a clarification as a created agent. - Prefer a persisted selection id when one is returned. Candidate-backed options use
the returned
idasselection_id, allowing the server to reuse the compiled choice without another interpretation pass. Positional fallbacks (opt_0,opt_1, …) have no persisted candidate; for those, send the visible label in the request text. optionscan be empty, and is capped at 4. When it's empty you still get aquestion— fall back to a free-text input rather than rendering an empty picker.
Render the options, then re-POST with the picked label folded into the next
request string:
{
"request": "notify me when gas is cheap\n\nClarification — Which \"gas\" — Ethereum L1 gwei, or a specific chain?: Base gas (gwei)",
"wallets": ["0x8f2a...d5"]
}
When the selected option has a persisted candidate id, include it too:
{
"request": "notify me when gas is cheap",
"selection_id": "sel_...",
"wallets": ["0x8f2a...d5"]
}
Same inline convention /intent's clarification flow uses — there's no server-side
conversation state to carry, so the answer travels inside the next request string.
That string is re-compiled as plain language, not parsed, so the exact
\n\nClarification — {question}: {label} template isn't magic — it's simply a shape that
compiles reliably because it keeps the original request and the answer together. Any
phrasing that states both will work; what fails is sending only the answer, since the
original intent is gone.
The reply can itself come back as another needs_clarification if it's still ambiguous.
Treat it as a loop with a cap — two or three rounds, then surface the question to a human
rather than retrying forever.
Whether a very broad request comes back as a 200 clarification or a 422 unwatchable is
not deterministic today — the same text can classify either way on different calls,
because an LLM decides whether it's a watchable condition at all before the clarification
step runs. Handle both: on 422, show the reason and let the user rephrase rather than
assuming the clarification round-trip is broken.
PATCH /agents/{id} with a new request (recompiling a condition agent's text)
follows the identical round-trip.
Display strings
Every agent carries a rendered one-liner suitable for showing a user — "BTC price is above $65,350", "aave health factor drops below 1.5". For plain-English agents it's
what the compiler produced from the request; for typed kinds Signo builds it from the
fields you sent, so you don't have to invent a label per kind. Plain-English agents also
carry the original intent.
Prefer rendered for display and fall back to your own string only if it's absent. It is
returned by GET /agents, GET /agents/{id}, and for in-page agents by surfaces/resolve
(existing.rendered) and an activated activation response, so one read is enough.
Treat it as display text, not data. Formatting is not guaranteed stable or consistent
between kinds — typed agents are formatted by us ("ETH price is above $1,973.26") while
plain-English ones are phrased by the compiler ("ETH price is above 5,000"), so currency
symbols and rounding can differ. Never parse rendered to recover a threshold or an asset;
every agent returns those as structured fields.
Typed kinds reference
Skip the plain-English compile step by sending a typed body instead —
{ kind, wallets, ...fields }, 201 immediately, no LLM call. All fields are
snake_case, same as the rest of /v1beta.
kind | Required fields | Notes |
|---|---|---|
price | asset_symbol, coingecko_id, condition (above|below), threshold | coingecko_id must be the literal CoinGecko id ("ethereum", not "ETH") — look it up yourself, e.g. via CoinGecko's /coins/list. Optional pct_change/base_price to trigger on a percent move instead. |
yield | pool_id, protocol, chain, asset, condition, threshold | Optional metric: apy (default) | supplyApy | borrowApy (these three values are the literal wire values — not converted). |
health | protocol (aave|spark), chain, threshold | Fires when the health factor drops below threshold — a decimal like 1.5, never a percent (values ≥ 100 are rejected as a likely mistake). Optional target_wallet if the address to watch differs from the owner wallet in wallets[0]. |
portfolio | threshold | Percent move, always the "above" direction (no down-only portfolio agent). Optional asset_symbol to scope to one holding instead of the whole portfolio; optional movement_window (24h default | 7d). |
schedule | cadence (daily|weekly|monthly), hour, minute, timezone | A recurring notify-only check-in, not a market condition. timezone is required and explicit here (contrast the plain-English schedule path, which anchors to UTC noon since there's no browser timezone to read from a raw API call). day_of_week required for weekly, day_of_month for monthly. Optional label for a display name. |
condition (the plain-English kind) isn't a typed-create option — you only get one by
sending request instead of kind.
Every agent's wallets array preserves the case you sent, for every kind (Solana
addresses are case-sensitive).
When an agent actually triggers
Worth understanding before you test, because it's the difference between "broken" and "working exactly as designed".
Evaluation runs on a schedule, not continuously. Typed price agents run hourly
because their price snapshot is refreshed hourly. Notify/Prepare yield and health-factor
agents normally run about every 5 minutes. Portfolio-movement agents run hourly. Shield
Execute agents normally run hourly, with the
continuation exceptions.
Plain-English condition agents use their stored cadence floor, usually hourly or
daily. An agent's last_checked_at advances on each pass whether or not it triggers.
After the first check, triggers are edge-based, not level-based. A typed price agent can fire on its first hourly check when the price is already on the trigger side. After that, it triggers when the condition becomes false and then true again.
Three consequences:
- The first typed-price check can trigger. If the price is already on the requested side at the first hourly evaluation, the agent can fire then.
- Movement between checks is invisible. If the price crosses your threshold and comes back between hourly price snapshots, both samples land on the same side and nothing triggers. Polling a price feed every 20 seconds will show crossings we never see.
- Agents re-arm. They are not one-shot: each genuine crossing triggers again. A typed percent-price agent keeps the threshold supplied at creation; it re-arms when that condition becomes false and fires when it becomes true again. It does not ratchet the threshold after a firing.
Use POST /agents/{id}/test to verify your integration. It's deterministic and
immediate, and it exercises the identical delivery path a real trigger uses. For an
in-page agent with a prepared action it also runs the preparation, so the test message
carries a prepared result. It does not evaluate the condition as a gate: the test fires
either way.
For a natural typed-price trigger, wait for the next hourly evaluation. It can fire on that first check when already true; later firings need a genuine false-to-true crossing between hourly snapshots. Do not wait on a market move to prove an integration: use the deterministic test endpoint above.
Health, yield and portfolio agents follow the same evaluate-then-compare rhythm on their own cadences.
Quotas
- Active agents per tenant: 50 (beta) / 500 (standard) / 5000 (high), by your
key's rate tier.
POST /agentspast the cap returns429 agent_quota_exceeded— pause or delete one first. - Webhook endpoints per tenant: 5.
POST /webhook-endpointspast the cap returns429 endpoint_quota_exceeded— remove one first (GET /webhook-endpointsshows what's registered). - Rate limit: the same per-key tier as the rest of
/v1beta(GET /metafor the exact numbers) — a429 rate_limitedhere is unrelated to the agent-count quota above;POST /agentscan return either code at429. - Credits: none of this — agents, events, and webhook-endpoint management are not metered against your credit balance at all.
Errors
The codes you'll actually meet on these endpoints. error is the stable machine value —
match on it, not on friendly_message, which is prose and may change.
| Status | error | Means | Do |
|---|---|---|---|
401 | invalid_key | Missing or malformed bearer key | Check the Authorization header |
403 | scope_forbidden | Key isn't authorized for agents | Ask us to add the scope — not a code bug |
404 | agent_not_found | No agent with that id for your tenant | Check you sent the full w_{type}_{id} |
400 | invalid_request | Body failed validation | Read issues[] — it names the field |
429 | agent_quota_exceeded | At your tier's active-agent cap | Pause or delete one |
429 | rate_limited | Per-key request rate | Back off and retry |
402 | insufficient_credits | Tenant is out of credits | Partner ops — don't surface to end users |
Requests from a region where the service is unavailable return HTTP 451 at the edge. That response contains an error message but not the normal application error envelope. Handle the status before matching the table above.
404 agent_not_found is deliberately returned for an agent that exists but belongs to
another tenant — we never distinguish "not yours" from "not there", since that would leak
whether an id is real.
Prepare agents: a trigger that pre-plans a transaction
Agents default to Notify: a fire tells you something happened, and what to do
about it is on you. Set autonomy: "prepare" and the agent also stores an action.
When it fires, Signo runs the action through the same planner the app uses and hands
you an unsigned plan to review and sign. Nothing is ever signed or broadcast on
your behalf — every execution still needs a wallet signature.
{
"kind": "price",
"wallets": ["0x1111111111111111111111111111111111111111"],
"asset_symbol": "ETH",
"coingecko_id": "ethereum",
"condition": "below",
"threshold": 3000,
"autonomy": "prepare",
"action": "Swap 10 USDC to ETH on Arbitrum",
"idempotency_key": "eth-entry-v1"
}
Plain-English requests can carry the action in request instead
("when ETH drops below 3000, swap 10 USDC to ETH on Arbitrum") — but Prepare is
never inferred. Without autonomy: "prepare" a "when X, do Y" request is armed as
notify-only and the action half is dropped, exactly as before. With it, the action is
required: an explicit Prepare with no action returns 422 action_required, and an
action on a Notify agent returns 422 action_requires_prepare. autonomy: "execute"
is not accepted here (422 execute_requires_shield) — automatic execution needs a
signed Shield permission, see Shield below.
What is stored is an instruction, not a quote. Balances, routes, amounts and any clarification are resolved when the agent fires, against the wallet's state at that moment. Naming a chain in the action does not add support for a protocol or an action the planner does not already have.
Fire, plan, deliver. A fire (real or via POST /agents/{id}/test) queues one
planning job, which retries transient failures up to three times. The outcome is
published as an agent event on both rails — webhook and GET /agent-events — with a
preparation object carrying the outcome and, when a plan was built, a private
review_url:
preparation.outcome | Means |
|---|---|
ready | A plan was built. Open review_url to see refreshed quotes and sign. |
clarification | The planner needs one answer before it can plan (an amount, a venue). |
informational | Nothing to plan — the planner answered with information instead. |
failed | Planning failed (including insufficient credits). Top up, then POST /agents/{id}/test to try again. |
cancelled | The agent was paused, edited or deleted while the job was in flight. |
The event's created_at is publication time (after planning); preparation.triggered_at
keeps the original trigger time. Review URLs are bearer links: anyone holding one
can open the review, so do not publish them. They expire after seven days, and the
review page regenerates quotes before asking for a signature.
Editing. GET and list responses include autonomy and action. PATCH /agents/{id}
accepts an autonomy / action change on any tenant-owned agent; omitting them on a
trigger edit keeps the existing action. Set autonomy: "notify" to drop the action.
Cost. Creation and Notify fires are quota-gated, not metered. A ready outcome is
charged the standard 5-credit partner planning rate, once per fire; clarifications,
informational and failed outcomes and cancelled jobs are not charged.
Delivery. Partner-created agents deliver on webhooks or event polling. They need no Telegram account, unlike the keyless drafts below.
In-page agents created through surfaces/activate keep their own model: a catalog action
with the prepare capability pre-plans on fire and hands back a reviewable result. See
Prepared results.
Keyless drafts (no API key)
An assistant with no key can draft a Notify or Prepare agent for any wallet and hand the owner a review link; the same call is POST /agent-drafts and the public MCP tool signo_draft_agent. That flow is documented for its own audience under For AI assistants. Two things matter from the partner side: a claimed draft belongs to the owner, not to a tenant, so it never appears in your GET /agents; and your keyed MCP client does not carry the public tools, so add the server a second time without the header if you want them.
Shield: permissions and protection
Automatic execution — an agent that spends without a signature at fire time — is only available through Signo Shield, an on-chain permission the owner signs once. The API and MCP let a caller prepare a Shield proposal and, with a partner key, read its state; authorizing it is always the owner, on the review page, with their wallet.
Drafting a permission needs no API key. POST /shield/proposals with no
Authorization header, or the public MCP tool signo_draft_permission, takes the
permission shape below and returns a private review_url. It is rate limited like
keyless drafts, the proposal expires after 72 hours, and
it is stored under a public tenant that no key can read — the owner reads it on the
review page and signs there. The three reads need a key.
New Execute setup is in early access. Before storing a proposal, the service checks the
named wallet. A wallet without access gets 403 execute_team_only with the early-access
message, and no proposal is stored. Direct the owner to support@signo.fi rather than
retrying the same request. Existing live permissions continue under their signed terms.
POST /shield/proposalsstores a proposal and returnsproposal_id, a privatereview_url, a plain-Englishsummary, thelimits, the livefee_bpsandexpires_at(unix milliseconds). It never authorizes spending. Free. With no key it takes the permission shape only; with a partner key (agentsscope) it takes either shape.GET /shield/proposals/{id}reads the proposal.GET /shield/proposals/{id}/permissionreads the permission once authorized (nullbefore).GET /shield/proposals/{id}/runslists what the permission has executed, most recent first (limit1 to 100).- All reads are tenant-scoped: a proposal you did not create is
404.
The request is a permission (ShieldPermissionRequest, schema_version: 2): an explicit
trigger (a supported plain-English condition, compiled like an in-app Execute agent with the part the Shield can read pinned on chain and the rest decided by scheduled checks; or a typed health_factor, token_balance, or price: the asset's current USD price from the Aave oracle on the permission's chain, Chainlink-backed). It does not make every Notify preset an Execute trigger: Execute is not available for the app's Portfolio move trigger, boxed Pendle PT loops are notify-only today, and a recurring schedule is not an Execute trigger. The request also includes an action (transfer, swap, vault,
supply, repay_from_wallet or repay_from_collateral), a lifetime budget and a
per-execution cap_per, both before Shield fees, and an expiry_days (1 to 90, default 30). The older protection
short-form (a body with no schema_version) is retired; a body in that shape answers
400 with the mapping — its wallet and collateral actions are repay_from_wallet and
repay_from_collateral here (the old names are still accepted as aliases), with a health_factor trigger. Today Shield runs on X Layer
(chain_id: 196) and Arbitrum One (chain_id: 42161), for a single wallet. The action kinds: transfer sends the asset to one
fixed recipient (never the owner wallet itself); swap goes through OKX DEX into
token_out and takes exactly one of min_out or max_slippage_bps; vault deposits
into an ERC-4626 vault; supply supplies into Aave; repay_from_wallet repays Aave debt from the
wallet's own balance; repay_from_collateral repays Aave debt by selling collateral down to a
target_health_factor.
The access, asset and budget checks run before anything is stored, and a refusal stores nothing:
- Assets go by the symbol Signo uses for them on X Layer (
XETH,XBTC,USDT0,WOKB,USDG,USDC,GHO,XSOL,XBETH,XOKSOL;USDTis accepted forUSDT0, bareBTCandETHresolve toXBTCandXETH, and a swap output ofOKBbecomesWOKB), by their ERC-20 address, or by any symbol on OKX's X Layer token list (about 660 tokens:AMDx,INTCx,IBMx,DMCX, …). One match resolves to that token; when several listed tokens share a symbol the request answers422 needs_clarificationandissues[]lists the candidates with their addresses and token pages, so pass the address you mean. The symbols are names for resolving addresses, not an allowlist: any ERC-20 on X Layer works fortransferandswap; Aave actions need an Aave reserve. - On Arbitrum the symbols Signo uses are
WETH,USDC,USDT,WBTC,ARBandDAI;USD₮0andUSDT0resolve toUSDT, bareBTCresolves toWBTC, and a swap output ofETHbecomesWETH. The fallback is LI.FI's Arbitrum token list. The rest works the same way. - Budget arithmetic. Both amounts are before fees.
cap_permust be positive and no greater thanbudget(it may equal it). At 10 bps,cap_per: "100"andbudget: "100"is valid: the registered lifetime cap and token approval cover100.1. The fee buffer is rounded up to the token's smallest unit. A positivecap_perabovebudgetreturns400 invalid_budget.
A native coin cannot be spent by Execute. Shield moves approved tokens. For a transfer, supply, repayment or swap input, wrap OKB to WOKB on X Layer or ETH to WETH on Arbitrum; an attempt to spend native OKB or ETH is refused at setup. A swap that buys the chain's own coin is different: Signo drafts WOKB or WETH as the output, and the review page names the wrapped token before the owner signs.
The owner wallet does not need to hold the budget when the proposal is made: every firing checks the owner's balance and allowance for that firing, and one the wallet cannot pay does not run. The permission registers on Shield v1.
What the owner signs is exactly the summary shown on the review page: the target and
spender are pinned, every execution is capped, the lifetime budget is enforced on-chain,
and a firing that would breach any of them is cancelled by the contract. fee_bps is
stamped into the permission at signing; see Fees for the current rate. A
proposal that cannot be compiled returns 422 with the compiler's reason.
Explain firing behavior before authorization. An already-true condition can fire at the first hourly check. Start after Pause re-arms it, so it can fire at the next check without first turning false. One successful execution completes a trigger by default. Afterward, an agent that stays active normally waits for a false reading and a new crossing. An "any X" condition or two to six top-level OR parts can instead fire for each new matching item, one run at a time, while the overall condition stays true. Waiting items are checked again after a four-minute cooldown; an item that stops matching can fire again when it returns. Repay from collateral continues toward its target only when the owner turns on Keep going until the target in the app before signing. The option is off by default, older permissions behave as off, and the public API and MCP request shape do not opt in. With it on, an Aave-limited partial firing can continue after that cooldown, within the signed limits, until its target, an owner-limit stop, ten firings or six hours. A check that could not act because of sizing, balance, approval, a venue stop or a service-wide execution pause can try again while the condition stays true. A sent firing that fails is retried once at the next hourly check. The signing service allows at most two execution attempts for one permission in a rolling hour. After each successful firing the owner receives Goal, Done, Remaining, Reasoning and Next, including the confirmed transaction, what limited a partial result and whether another execution is possible. See Execute checks.
Pausing, resuming, deleting
PATCH /agents/{id} with { "status": "paused" } or { "status": "active" } works
for supported Notify/Prepare agents your key is authorized to manage, including
in-page agents. It does not grant control of an owner-only Execute agent, an
Execute setup in progress, or an agent with a linked permission. A free-text request edit is
refused for an in-page agent (400 invalid_patch); change those through
surfaces/activate. DELETE /agents/{id} removes it and is
idempotent — a repeated delete, or one aimed at an id you don't own, still returns
204. For owner-only Execute rows, pause and edits return not found; delete
returns 204 without removing the agent. A successful delete response is not proof
that an on-chain permission was revoked. The owner uses Signo to manage that authority.
Both operations are described in full in the API Reference.
MCP
Everything above is also available as MCP tools on Signo's hosted server at
https://app.signo.fi/api/mcp (Streamable HTTP). The tool set depends on whether you
send a key:
No Authorization header — three public tools. wallet_insights_report is the read-only wallet report, whose suggestedAgents feed straight into the drafting tool. signo_draft_agent is the MCP form
of keyless drafts: { request, wallet?, autonomy? } in, a
draft with a review_url out. signo_draft_permission is the MCP form of a
keyless POST /shield/proposals in the permission shape: a stored proposal with a
review_url out, nothing authorized until the owner signs. The Execute early-access
check applies here too: a wallet without access gets 403 execute_team_only, and no
proposal is stored. The public tools expose no
account-management or private proposal reads and cannot spend; the wallet report reads public data.
An assistant can add the server with no configuration at all and prepare an
agent or a permission for its user to claim.
With a partner key — the full set. Same capabilities, same key, same quotas as REST:
| Tool | Same as |
|---|---|
signo_agent_create | POST /agents (Notify or Prepare) |
signo_agent_list | GET /agents |
signo_agent_get | GET /agents/{id} |
signo_agent_update | PATCH /agents/{id} |
signo_agent_delete | DELETE /agents/{id} |
signo_agent_test | POST /agents/{id}/test |
signo_agent_events | GET /agent-events |
signo_agent_catalog | GET /agent-catalog |
signo_webhook_endpoint_create | POST /webhook-endpoints |
signo_webhook_endpoint_list | GET /webhook-endpoints |
signo_webhook_endpoint_delete | DELETE /webhook-endpoints/{id} |
signo_draft_permission | POST /shield/proposals (also public) |
signo_shield_proposal | GET /shield/proposals/{id} |
signo_shield_permission | GET /shield/proposals/{id}/permission |
signo_shield_runs | GET /shield/proposals/{id}/runs |
The tools were called signo_agent_prepare and signo_draft_permission until 2026-09-22; those names still work, unlisted, until 2026-10-06.
A bad or expired key is refused with 401 — it never falls back to the public tool
set, so a misconfigured client cannot quietly lose its capabilities. The reverse also
holds: a valid key replaces the three public tools, they are not in a keyed client's
tool list. A client that already has a key and also wants to draft agents for other
people's wallets adds the server a second time, without the header:
claude mcp add --transport http signo-public https://app.signo.fi/api/mcp
An agent working through these tools gets the same recommendation as the REST
quickstart above: call signo_webhook_endpoint_create first so triggers push to you,
and fall back to signo_agent_events only if you'd rather poll. Add the server
once and all of the tools above (plus signo_intent/signo_plan/signo_simulate/
signo_reencode/signo_usage) come with it:
claude mcp add --transport http signo https://app.signo.fi/api/mcp \
--header "Authorization: Bearer sg_beta_..."
See API (beta) for the Cursor config and more on the hosted MCP server.
Reference
- API Reference — full request/response schemas, every error
code, and the
webhooksdelivery contract, generated from the OpenAPI spec - OpenAPI 3.1 spec:
https://app.signo.fi/signo-openapi.yaml(source of truth) - Machine-readable index for agents:
https://app.signo.fi/llms.txt