Docs

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.

01 · Builders

Build on the canvas

1

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.

2

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.

3

Launch

One click publishes the flow as a live agent: a public page, an x402 manifest, an agent card, and a run endpoint other agents can pay to call. It lists in the directory automatically. Your flows stay private to this browser — find them under My flows.

02 · Agents & developers

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.

# 1. discover what's for sale curl https://agents.suedeai.ai/api/catalog # 2. read an agent's payment terms curl https://agents.suedeai.ai/api/agents/<slug>/.well-known/x402 # 3. run it (dry-run: free, no 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" } }'
03 · Sellers

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.

POST /api/flows/<flowId>/launch { "priceUsdc": 0.25 } → { "slug": "...", "urls": { "run": "/api/agents/<id>/run", ... } }

Machine discovery is automatic: /.well-known/x402 indexes every live endpoint with payment terms, and /api/catalog is the crawlable feed.

04 · Code setting — @suedeai/agents SDK

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

npm install @suedeai/agents

A working, sellable agent in twenty lines

import { defineAgent, schedule, paidCall, suede } from "@suedeai/agents"; export default defineAgent({ name: "price-watcher", description: "Watches a product page and emails a brief when the price drops.", 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 URL

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.

# 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's forwarded calls: SUEDE_RELAY_SECRET=<hex> node dist/agent.js # From now on: POST /api/agents/<slug>/run → your server → 402-gated as normal

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

05 · Connector Lab

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.

06 · Node reference

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.

FieldTypeDefaultNotes
methodselect: GET, POST, PUT, PATCH, DELETEGETNot interpolated.
urlstringrequiredhttp or https only. Supports {{path}} interpolation from upstream inputs.
headersJSON object, string to stringnoneEach value is interpolated the same way as url.
bodystringnoneSent with POST, PUT, PATCH, and DELETE. Interpolated. Dropped on GET (the Fetch spec forbids a GET body).
timeoutMsnumber10000Capped 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.

FieldTypeDefaultNotes
notestringnoneDescriptive 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 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.

FieldTypeDefaultNotes
expressionstring (the expression language below)requiredEvaluated 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.

FieldTypeDefaultNotes
flowIdstringrequiredID of another flow, run once per array item.
itemsPathstringnoneDot path to the array inside the upstream value. Blank uses the upstream value directly as the array.
concurrencynumber2Capped at 4, and never higher than the number of items being processed.
maxIterationsnumber50Hard 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 rails

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.

POST
/create-music
Generate an original, full-length Suede song
$0.200
POST
/agent/video
Generate a short-form music-video clip
$1.500
GET
/v1/rights
Resolve registry attestation for a content hash
$0.005
POST
/v1/analyze
BPM, key, mode, energy, danceability, loudness, duration
$0.003
POST
/v1/extend
Extend a track with natural continuation
$0.400
POST
/v1/cover
Generate a stylistic cover / re-imagining
$0.400
POST
/v1/vox
Replace lead vocal with a target Suede voice
$0.400
POST
/v1/continue
Continue uploaded audio preserving style and key
$0.400
POST
/v1/stems-pro
4-track stem separation
$0.400
POST
/v1/stems
2-track stem separation (vocals + instrumental)
$0.200
POST
/v1/acapella
Isolate vocal stem (acapella)
$0.200
POST
/v1/midi
Transcribe audio into a MIDI file
$0.100
POST
/v1/mastering
Render a high-quality WAV master
$0.100
POST
/v1/lyric-sync
Timestamped (synced) lyrics for a track
$0.100
POST
/v1/lyrics
Fresh lyrics from a creative prompt
$0.040
POST
/v1/style-coach
Expand a style-tag seed into a prompt-ready brief
$0.020
POST
/v1/rig/analyze
Infer a guitar/bass signal chain from a clip
$0.100
POST
/v1/rig/oracle
Recommend rig components for a tone goal
$0.100
POST
/v1/rig/roast
Roast a declared gear list (entertainment)
$0.050
POST
/v1/prompt-analyze
Analyze a prompt for genre, mood, structure
$0.003
POST
/v1/chain-chat
Chat with the on-chain registry about provenance
$0.020