From canvas to paid endpoint.
Four ways in: build on the canvas, write TypeScript and push from the terminal, call a published agent, or publish your flow so other agents pay you to run it. Everything works in dry-run with zero configuration.
Build on the canvas
Start from a template or a blank canvas
Input → LLM → Output scores a lead in one call — one of 76 templates. Drag nodes from the palette, or start the music/IP loop with Song → Register IP → Royalty Split. Every Suede rail is a node with its USDC price on the card.
Run it and watch the ledger
The run dock streams every node live with a per-node cost ledger. Dry-run is the default: no USDC moves, the flow logic and pricing are real.
Call a published agent in three steps
No API keys, no signup. Priced calls settle in USDC on Base over x402; while an agent is in dry-run mode the same endpoint responds free so you can integrate first and pay when it goes live.
Publish your flow as a paid endpoint
Launching from the studio takes an optional per-call price. Relaunching the same flow updates the price and keeps the slug — your integration URLs never break.
Machine discovery is automatic: /.well-known/x402 indexes every live endpoint with payment terms, and /api/catalog is the crawlable feed.
Write an agent in TypeScript. Push it from the terminal.
The Code setting is the third way to build on Suede. Install the SDK, define your agent in a agent.ts file, and push it live with one command — no API keys, no Stripe onboarding. Suede provides the model.
Install
A working, sellable agent in twenty lines
API surface (v0)
CLI commands
Relay: earn through Suede while running your own server
Link your self-hosted agent server to the platform so callers use the Suede endpoint (402-gated, paid) but execution happens on your machine. Suede verifies the payment, forwards the call with an HMAC signature, and routes the USDC to your payout address.
Environment variables
Gateway pricing
The Suede gateway provides the LLM — no API key needed. First 100k tokens/month are free per workspace. After that: $10.80 per 1M tokens (billed via the credit topup endpoint — HTTP 402, USDC on Base). Platform take on agent calls: 5% of settled price (creator keeps 95%).
Typed API operations, simulated locally
Connector Lab is a default-off prototype for importing one bounded OpenAPI 3.1.0 JSON operation as an immutable typed node. Every API Operation is labeled Prototype: simulation only. Simulation validates the typed request, creates a redacted plan, and passes trusted schema-shaped output to local downstream steps. The Run Dock says Simulated locally. No request sent.
An optional readiness check can confirm only that a compatible Test slot is configured. Its receipt says Test slot configured. Authentication unverified. It does not decrypt a credential, log in, check provider health, or send a request. API Operation cannot run in published, Live, or durable workflows, and this prototype is not a broad OpenAPI or connector-parity claim.
The kernel adds no required paid service. The exact clean local gate is npm run verify:phase4b1; it uses process-level guards, not an OS-level network sandbox. Operator compute and storage may still cost money.
General-purpose nodes: HTTP, Webhook, Transform, Loop
These four nodes are what make a flow general-purpose instead of music-specific: call any REST API, receive events from a third party, reshape data between steps, and iterate over a list.
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 is the one exception worth calling out by name: it always runs for real (it has to, so its inner nodes get a chance to see dry-run mode themselves), and it passes dry-run through unchanged to every iteration's nested run, so a dry-run loop still fully iterates, it just never lets an inner HTTP or LLM node spend anything while doing so.
HTTP Request
Calls any REST API over http or https. This is a free node (Suede 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, not just the hostname string, and the same check runs again before every redirect hop (up to 5 redirects). This narrows but does not close DNS rebinding: validation and the actual connection are two separate DNS lookups a few milliseconds apart, so a resolver that changes its answer inside that window could still slip through. Closing that gap fully would need pinning the socket to the exact validated IP, which this pass does not do.
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.
| 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.
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 dry-run mode is decided by the agent's own settlement setting, never by the caller. An agent that hasn't gone live stays free no matter what a third party sends, and a live agent is never forced back into a free run either.
Security note: the secret is stored as a hash, but that stored value is also the literal HMAC key used to verify every request, so a database compromise gets an attacker forgeable signatures. That is the same property Stripe's and GitHub's webhook secrets have; there is no way to have a recoverable HMAC key that a database read can't also recover.
Transform
Reshapes data between steps, pluck a field, build a new object, filter or format a list, without a round trip through an LLM. This is not a code-execution node: the expression language is a small, non-Turing-complete grammar. There is 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 50ms 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 allows only one level of subflow/loop nesting total. That means a flow can call a loop whose subflow runs fine, but that subflow cannot itself contain another loop or subflow node. Trying to would fail just that 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 still iterates fully, it just never lets an inner cost-bearing or side-effecting node spend anything.
The 21 Suede endpoints behind the nodes
Studio nodes call these pay-per-call endpoints on api.suedeai.xyz through the x402 client — dry-run by default, live settlement when enabled.