Developer quick reference Everything you need to ship an agent.
Guided walks you through software setup. Expert tools open the canvas in Studio or TypeScript in Code. Switch views on the same saved workflow. Publish it, then call it with one HTTP request. Want to see it working first? Six worked examples, from invoice chasing to one agent paying another.
- What you'll do
- Pick your path below, then use this page as the quick reference: quickstarts, the SDK and CLI, field-level node detail, and the public gateway routes.
- What you'll have
- Commands you can copy as they are, and a link to the guide that goes deeper on each topic.
Quick definitions: dry run, AgentCard, and MCP
- Dry run
- A test run that executes your flow's logic for real but stubs every step that would cost money or reach an outside system, so nothing is spent.
- AgentCard
- A small JSON document describing an agent (its name, skills, and how to call it) so other agents and tools can discover it.
- MCP
- Model Context Protocol, an open standard that lets AI clients such as Claude or an IDE assistant call external tools.
Every guide, one topic per page
This page is the quick reference. The guides go deeper, one topic per page:
Long-form writing on agentic workflows, flow design, and agent commerce lives in the articles.
Build on the canvas
Start from a template or a blank canvas
Input → LLM → Output scores a lead in one call. Drag nodes from the palette, or start a multi-step workflow with Brief → Campaign Draft → Output. Every Suede media step is a node with its per-call price on the card.
Run it and watch the ledger
The run dock streams every node live with a per-node cost ledger. Dry runs are free: the flow logic and pricing are real, and nothing is charged.
Launch
One click publishes the flow with a public page, explicit call-state discovery, an AgentCard, and a run endpoint. Standalone services open as a free preview. Add optional per-call payment and the agent publishes its x402 terms; company-backed services read as unavailable until payment is on. It stays unlisted unless you choose to list it in the directory. Your flows stay private to you (tied to this browser until you sign in, then to your account); find them under Workspace.
Call a published agent in three steps
Discovery tells you each service's state. A preview service runs free as a dry run, with no API key or wallet. When an owner adds optional per-call payment, the service quotes x402 v2 terms; buyers pay from usage credit on its page or in USDC on Base.
# 1. discover published agents and their payment state
curl https://agents.suedeai.ai/api/catalog
# 2. read an agent's payment state and active terms, when enabled
curl https://agents.suedeai.ai/api/agents/<slug>/.well-known/x402
# 3. run it (preview: free dry-run, no key or wallet needed)
curl -X POST https://agents.suedeai.ai/api/agents/<id>/run \
-H 'content-type: application/json' \
-d '{ "input": { "prompt": "Q3 renewal for Acme Corp, 12 seats, auto-renews in 45 days" } }'
# "prompt" is the default for flows with no configured Input fields.
# Each agent's real field names are on its page and in its .well-known/x402.Publish first. Add per-call payment if you want it
Launching gives the agent its own page, API and run endpoint, and takes an optional per-call price. Relaunching the same flow updates the agent and keeps the slug, so your integration URLs never break.
POST /api/flows/<flowId>/launch
{ "priceUsdc": 0.25 }
→ { "slug": "...", "urls": { "run": "/api/agents/<id>/run", ... } }Machine discovery follows the directory listing. The /api/catalog is the crawlable feed, and /.well-known/x402 indexes listed endpoints with their call state, plus active terms for agents sold per call.
Write an agent in TypeScript, push it from the terminal
The Code setting is the third way to build on Suede AI. Install the SDK, define your agent in an agent.ts file, and push it live with one command: no model API keys, no payment setup (you log in once with a workspace key). Suede AI provides the model. The SDK and CLI guide walks through every command step by step.
Install
npm install @suedeai/agentsA working, sellable agent in sixteen lines
import { defineAgent, schedule, paidCall, suede } from "@suedeai/agents";
export default defineAgent({
name: "price-watcher",
description: "Extracts a price from supplied text, remembers it, and flags a drop.",
triggers: [schedule("0 13 * * *"), paidCall(0.25)],
async run({ input, memory }) {
const out = await suede.llm({
system: "Extract the price as a number.",
prompt: String(input ?? ""),
});
const last = await memory.get<number>("lastPrice");
await memory.set("lastPrice", Number(out.text));
return { price: out.text, dropped: last !== undefined && Number(out.text) < last };
},
});API surface (v0)
// Core exports
defineAgent(def: AgentDefinition): Readonly<AgentDefinition>
schedule(cron: string): Trigger // five-field cron (UTC)
paidCall(priceUsdc: number): Trigger // price >= 0
manual(): Trigger
webhook(): Trigger // relay-driven (suede link)
// LLM gateway
suede.llm({ system: string; prompt: string }): Promise<{ text: string }>
suede.run(nodeType: string, config?: unknown): Promise<{ output: unknown }>
// Local dev
createLocalMemory(workdir?: string): AgentMemory // .suede/memory.json
// Self-host
serve(agent: AgentDefinition, { port: number }): ServeHandle
// POST /run · { input?, trigger? } → { output }
// GET /manifest · agent metadata (no run fn)CLI commands
suede init # scaffold agent.ts + .suede/config.json
suede login <key> # save workspace key (from agents.suedeai.ai/flows)
suede push # publish agent.ts → live endpoint
suede pull <slug> # write manifest.json + agent.ts from platform
suede dev # local serve on :3001 (POST /run, GET /manifest)
suede whoami # show active key prefix + API URLRelay: earn through Suede AI while running your own server
Link your self-hosted agent server to the platform: callers use the Suede AI endpoint and execution happens on your machine. Previews forward free. When you switch on optional per-call payment, Suede AI verifies each x402 payment, forwards the call with an HMAC signature, and routes the settled USDC to your payout address.
# Link your local server to the platform so callers hit YOUR machine:
suede link <slug> --url https://your-server.com/run
# → prints: SUEDE_RELAY_SECRET=<hex> (save this)
# Start your server with the secret to authenticate Suede AI's forwarded calls:
SUEDE_RELAY_SECRET=<hex> node dist/agent.js
# From now on: POST /api/agents/<slug>/run → your server
# (402-gated only if you switch on optional per-call payment)Environment variables
# Required for live gateway calls
SUEDE_WORKSPACE_KEY=<key-from-flows-dashboard>
# Optional overrides
SUEDE_GATEWAY_STUB=1 # offline echo mode: no HTTP, no key
SUEDE_API_URL=https://... # override platform URL (default: agents.suedeai.ai)
SUEDE_RELAY_SECRET=<hex> # authenticate relay-forwarded calls in serve()Gateway pricing
The Suede gateway provides the LLM, so you bring no API key. Inside a flow run, each call is paid from your usage credit at the model's provider list rate. Workspace runs apply your plan's platform fee (base 20%; Agency Scale 15%). Direct gateway calls use your plan's model rate: Claude Sonnet 4.6 costs $3.60 in / $18.00 out per 1M tokens at the base 20% markup, or $3.45 in / $17.25 out per 1M tokens with Agency Scale's 15% markup. Review your balance and any credits included in your plan on the usage page. A call's maximum cost is set aside before it runs, so every model call is fully funded before it starts.
Typed API operations, simulated locally
Connector Lab turns one OpenAPI 3.1.0 JSON operation into an immutable typed node, so you design a flow against a real API contract before any credential is involved. Every API Operation is labeled Prototype: simulation only. Simulation validates the typed request, builds a redacted request plan, and passes schema-shaped output to the downstream steps. The Run Dock confirms Simulated locally. No request sent.
A readiness check confirms that a compatible Test slot is configured; its receipt reads Test slot configured. Authentication unverified. API Operation stays in Test, so published, Live, and durable workflows run on production nodes. Connector Lab is off by default and adds no paid service; the local verification gate is npm run verify:phase4b1.
General-purpose nodes: HTTP, Webhook, Transform, Loop
These four nodes connect a flow to everything else: call any REST API, receive events from a third party, reshape data between steps, and iterate over a list. The full field-level reference for each one folds open below; the catalog-wide view by group lives in the node reference.
Dry-run: what actually runs
A dry run does not treat every node the same way. The engine stubs out any node marked cost-bearing or side-effecting before it ever calls that node's real executor. HTTP Request is side-effecting (it can reach an arbitrary third-party URL), so in a dry run it never makes a real request; it returns a fixed placeholder instead, shown below. LLM is cost-bearing and does not call the model. Input, Output, Branch, Schedule, Webhook, Subflow, Transform, and Loop are not stubbed at all: they execute for real in a dry run, because none of them costs money or reaches an external system on their own. Loop deserves a mention by name: it always runs for real so its inner nodes see dry-run mode themselves, and it passes dry-run through unchanged to every iteration's nested run. A dry-run loop fully iterates while no inner HTTP or LLM node spends anything.
HTTP Request
Calls any REST API over http or https. This is a free node (Suede AI charges nothing for it; you pay whatever the target API charges), and it is the app's primary SSRF surface, so every request is validated before it's made.
| Field | Type | Default | Notes |
|---|---|---|---|
| method | select: GET, POST, PUT, PATCH, DELETE | GET | Not interpolated. |
| url | string | required | http or https only. Supports {{path}} interpolation from upstream inputs. |
| headers | JSON object, string to string | none | Each value is interpolated the same way as url. |
| body | string | none | Sent with POST, PUT, PATCH, and DELETE. Interpolated. Dropped on GET (the Fetch spec forbids a GET body). |
| timeoutMs | number | 10000 | Capped at 30000 regardless of what's entered. |
SSRF guard. The URL's scheme must be http or https. Requests are blocked to a fixed set of hostname literals (localhost, localhost.localdomain, ip6-localhost, ip6-loopback, and anything ending in .local, .internal, or .localhost) and to reserved IP ranges: loopback (127.0.0.0/8), the three RFC1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16, which also covers the 169.254.169.254 cloud metadata endpoint), 0.0.0.0/8, and the 100.64.0.0/10 CGNAT range, plus the IPv6 equivalents (loopback, unspecified, link-local, unique-local, and IPv4-mapped addresses resolved through the same IPv4 list). The hostname is resolved and the resolved address is checked as well as the hostname string, and the same check runs again before every redirect hop (up to 5 redirects).
A 303 redirect downgrades the method to GET and drops the body; every other redirect status preserves both. The response body is capped at 2 MB, read in a stream and aborted (not truncated) if it goes over; the request itself times out after the configured timeoutMs (10000 by default, 30000 max).
Output: { status: number, body: <parsed JSON if the response is application/json, otherwise the raw text> }
Dry-run: no request is made. The node returns a placeholder in the same envelope shape so a downstream node still typechecks, but the body content is a fixed marker, not a guess at what the real target would have returned:
{
"status": 200,
"body": {
"dryRun": true,
"note": "HTTP request skipped during dry-run; no real request was made.",
"method": "POST",
"url": "https://example.com/webhook"
}
}Webhook
A trigger node: an external service (GitHub, Stripe, Slack, your own backend) posts JSON to this agent's webhook URL and the flow runs with that body as its input. Authentication is not a node field. A signing secret is generated once, server-side, when you launch the agent, and shown to you exactly once. It cannot be recovered later; relaunching an agent that already has a webhook endpoint leaves the existing secret untouched rather than rotating it out from under whatever third party is already using it. The webhooks guide covers wiring Stripe, GitHub and your own backend end to end.
| Field | Type | Default | Notes |
|---|---|---|---|
| note | string | none | Descriptive only, for your own reference. Has no effect on verification. |
Calling it. POST to /api/agents/<agent-id-or-slug>/webhook with a JSON body (application/json, exactly, charset suffix aside; anything else is rejected) and two headers: x-suede-webhook-signature (sha256=<hex>) and x-suede-webhook-timestamp (Unix milliseconds, as a decimal string). The signature is an HMAC-SHA256, keyed by your webhook secret, over the exact string <timestamp>.<raw request body bytes, before any JSON parsing>. Binding the timestamp into the signature means a captured request can't be replayed with a new timestamp without also forging a new signature, and requests signed more than 5 minutes off the current time (either direction) are rejected as stale. The request body is capped at 256 KB. A bad signature, a stale timestamp, and a nonexistent agent all return the same generic 401, on purpose, so a caller can't use the response to enumerate which agent ids or slugs exist. Both the source IP and the agent id are separately rate-limited.
import { createHmac } from "node:crypto";
const secret = process.env.MY_STORED_SUEDE_SECRET; // shown once, at launch
const timestamp = Date.now().toString();
const rawBody = JSON.stringify({ event: "payment.succeeded" });
// Base string is "<timestamp>.<raw body bytes, before JSON parsing>"
const base = `${timestamp}.${rawBody}`;
const signature = "sha256=" + createHmac("sha256", secret).update(base).digest("hex");
await fetch("https://agents.suedeai.ai/api/agents/<agent-id-or-slug>/webhook", {
method: "POST",
headers: {
"content-type": "application/json",
"x-suede-webhook-timestamp": timestamp,
"x-suede-webhook-signature": signature,
},
body: rawBody,
});Output: the inbound JSON body, forwarded as-is (wrapped as { body: <value> } if the posted JSON wasn't a plain object).
Dry-run: not stubbed. A webhook delivery's payment mode is decided by the service's payment-enablement state, never by the caller. A preview stays free no matter what a third party sends, while an enabled service keeps its payment gate.
Rotation: issue a fresh secret any time with POST /api/agents/<id>/webhook/rotate, the same model Stripe and GitHub use for webhook secrets.
Transform
Reshapes data between steps: plucks a field, builds a new object, filters or formats a list, without a round trip through an LLM. It runs a safe, bounded, non-Turing-complete expression language instead of arbitrary code: no eval or new Function, no globals, no network or filesystem access, no require or import, no user-defined functions, and no loop construct beyond the single fixed-arity map() builtin.
| Field | Type | Default | Notes |
|---|---|---|---|
| expression | string (the expression language below) | required | Evaluated against the node's inputs; the result becomes this node's output. |
Grammar. Path access with in.field or in.items[0].id; arithmetic (+ - * / %); comparisons (== != < <= > >=); logical && and ||; unary ! and -; a ternary test ? a : b; object literals ({ key: value, ... }); and array literals ([ a, b, c ]). Builtins: map, len, upper, lower, trim, join, split, get, jsonParse, jsonStringify, number, string, default. map() is the one binder in the language: map(array, item => expr) evaluates its lambda body once per array element with item scoped to that call only.
{ email: in.user.email, count: len(in.items) }map(in.items, x => x.id)in.status == "active" ? "go" : "hold"Limits, all enforced before or during evaluation: 5000 characters of source, 1000 tokens, 24 levels of nesting, 500 AST nodes total, 20000 evaluation steps (this is what bounds map() fanning out over a large array), a 1000ms wall-clock budget per evaluation, 1000 items max for map()/join()/split(), and 200,000 characters max for jsonParse()'s input. __proto__, constructor, and prototype are denied everywhere, static and dynamic, so no path through the language can reach the prototype chain.
Dry-run: not stubbed. This is local computation only, so it always runs for real, dry run or not.
Loop
Runs another flow once per element of an upstream array and collects the per-element results in order. Without this node, a flow author has to pre-batch an entire array into one blob and ask a single LLM call to handle all of it at once, forcing an all-or-nothing retry on any hiccup. The loop node itself never makes a paid or external call; every dollar it can spend comes from the nodes inside the subflow it runs.
| Field | Type | Default | Notes |
|---|---|---|---|
| flowId | string | required | ID of another flow, run once per array item. |
| itemsPath | string | none | Dot path to the array inside the upstream value. Blank uses the upstream value directly as the array. |
| concurrency | number | 2 | Capped at 4, and never higher than the number of items being processed. |
| maxIterations | number | 50 | Hard ceiling of 200. Inputs longer than the effective cap are rejected outright, never truncated. |
Failure policy: collect errors, not fail fast. Every element is attempted; one flaky item does not stop the others. The loop node itself still reports success as long as it could start (a valid array, a loadable subflow, within the iteration cap). Per-element failures land in the errors output instead of failing the whole node. The cost trade-off is explicit: because every element still runs, the worst-case cost of a loop is always up to N times the subflow's cost, whether or not some elements fail.
Output: two separate outputs, result and errors. Each entry in result is the full outputs record from that element's nested run (keyed by the subflow's own internal node ids), or null if that element failed. Each entry in errors is { index, error }, where error names the subflow node that failed:
{
"result": [
{ "<subflow-node-id>": { "result": "..." } }, // item 0: succeeded
null, // item 1: failed, see errors
{ "<subflow-node-id>": { "result": "..." } } // item 2: succeeded
],
"errors": [
{ "index": 1, "error": "<subflow-node-id>: <message>" }
]
}Cost ceiling. Every run (top-level or nested) shares one in-run cost ceiling, checked before every cost-bearing node runs. The ceiling is the minimum of an absolute per-run cap (the RUN_COST_CEILING_USDC environment variable, $5 if unset or invalid) and the agent's own remaining daily budget at the start of the run. If a loop iteration's nested run gets aborted for hitting that ceiling, it is not treated as a per-element failure: no further iterations start, and the loop node itself fails with a message stating how many of the N iterations completed before the abort, so the abort propagates to the whole run instead of being swallowed by collect-errors.
Nesting. A loop's subflow runs one level deeper than the run that called it, and the engine bounds total subflow/loop nesting with a depth guard: 16 levels. Loops inside subflows (and loops inside loops) are legal up to that ceiling, all sharing the one in-run cost ceiling; a nested run that would cross the depth guard fails that one iteration, surfaced in errors, not the whole run.
Dry-run: not stubbed. The loop always runs for real so its inner nodes get a chance to see dry-run mode themselves; ctx.dryRun passes through unchanged to every nested run, so a dry-run loop iterates fully while no inner cost-bearing or side-effecting node spends anything.
The 3 public Suede gateway routes
The public App catalog exposes 3 priced routes on api.suedeai.xyz for music, video, and image generation, each at a fixed per-call price. Drop them into a flow as nodes and runs bill them to your usage credit; agents that pay for themselves can call them directly over x402.
Last updated .