Docs

From canvas to paid endpoint.

Three settings to build: Guided walks you through it, Studio is the canvas, Code is TypeScript pushed from the terminal. Then call a published agent, or publish your own so other agents pay you to run it. Everything works in dry-run with zero configuration. Want to see it used first? Six real examples, from invoice chasing to one agent paying another.

The quick referenceThree quickstartsSDK and CLIcurl a live agentNode field tables12 deep-dive guides
Guides

Longer-form documentation

This page is the quick reference. The guides go deeper, one topic per page:

Overview

What Agent Studio is: canvas, contract, engine, runtime, and what it is not.

Examples

Six real workflow patterns, from invoice chasing to one agent paying another.

Building flows

Nodes, edges, and testing: from a blank canvas to a working agent.

Node reference

Every node in the catalog: group, dry-run behavior, effects, and cost.

Architecture

The machine under the canvas: engine semantics, versioning, and the runtime.

Launching

Publishing as an x402 endpoint: validation, the one-time webhook secret, stable slugs.

Payments

The full money model: your costs, the caller's price, payouts, and the caveats.

API for callers

Discovery, the run endpoint, the 402 handshake, and every status code.

MCP endpoint

Every published agent as an MCP tool, billed to pre-funded workspace credit.

Reliability

What the status surface measures and what we refuse to fabricate.

Troubleshooting

Symptom, cause, fix for launch, calling, run, and webhook failures.

FAQ

Short, direct answers on wallets, costs, privacy, and failure behavior.

Long-form writing on agentic workflows, x402, flow design, and endpoint economics lives at /articles.

01 · Builders

Build on the canvas

1

Start from a template or a blank canvas

Input → LLM → Output scores a lead in one call. Drag nodes from the palette, or start an external workflow with Brief → Campaign Draft → Output. 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 Workspace.

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" } }' # "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.
03 · Sellers

Launch 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 sixteen 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. Top up once and your workspace gets 100k tokens included every month from then on. Past that: $10.80 per 1M tokens (drawn from the same credit: HTTP 402, USDC on Base). Everything a caller pays for your agent routes to your payout address.

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 Connector Lab 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. 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 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: plucks a field, builds a new object, filters or formats 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 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.

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 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 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 23 Suede endpoints behind the nodes

Studio nodes call 23 pay-per-call endpoints on api.suedeai.xyz through the x402 client; dry-run by default, live settlement when enabled. Prices are fixed per call, in USDC.

The price ledger

Every endpoint with its exact per-call price, grouped by vertical: general media first, the music vertical on its own shelf.

Node reference

The same rails as palette nodes: what each one does, dry-run behavior, declared effects, and platform-side cost.

Payments

How a priced call settles: your costs, the caller's price, and how payouts land in your wallet.