Fresh agent delegate — full Relay bootstrap
User: 'Sign me up for Relay and call me Alex.'
Agent: register_tenant → OTP lands in the user's real inbox → user pastes code → submit_verification_code → agent token minted → whoami confirms.
Relay exposes every product operation as an MCP tool. Point your agent at /mcp, hand it an agent token, and it can sign users up, read verification emails, and deliver working API keys — all in one chat turn.
# 1. Discover the base URL + MCP endpoint + docs (unauthenticated)
curl https://relay.cumulush.com/.well-known/relay.json
# 2. Prove the token works
curl -H "Authorization: Bearer agt_..." \
https://relay.cumulush.com/v1/whoami
# 3. Browse the provider catalog in chunks (see next section)
curl -H "Authorization: Bearer agt_..." \
https://relay.cumulush.com/v1/indexStep 1 is the only unauthenticated hop. Its response contains apiBase, mcpEndpoint, openapiUrl, docsUrl, and agentDocsUrl — everything a fresh agent needs to wire the rest up.# Step 1 — what categories exist right now?
# Returns: { categories:[{ slug, displayName, count, providerIds }],
# aliases:{ "hoster":"hosting", "mail":"email", ... } }
curl -H "Authorization: Bearer agt_..." \
https://relay.cumulush.com/v1/index
# Step 2 — full details for just the category you need.
# Optional: ?capability=postgres&pricing=free-tier
# Returns: { category, displayName, providers:[ProviderSummary...] }
curl -H "Authorization: Bearer agt_..." \
"https://relay.cumulush.com/v1/index/database?capability=postgres"
# Step 3 — pick one + sign up as usual.
curl -X POST -H "Authorization: Bearer agt_..." \
-H "Content-Type: application/json" \
-d '{"provider":"neon","input":{"name":"my-app-db"}}' \
https://relay.cumulush.com/v1/signupsThe MCP equivalents are list_categories and list_providers_by_category. list_categories does not take an agent_token — the overview is public discovery data.
Every category query is normalized server-side. Aliases like hoster → hosting, mail → email, or logs → observabilityresolve automatically so an agent's fuzzy guess still lands on the right chunk. The full alias map is returned on GET /v1/index.
| Slug | What goes here |
|---|---|
| database | Postgres / MySQL / key-value / managed DB of any flavor. |
| hosting | Deploy + serve an app. Vercel, Netlify, Fly, Cloudflare Workers. |
| Transactional email senders (Resend, Postmark, SendGrid). | |
| newsletter | Broadcast / marketing email (Buttondown, Substack, Beehiiv). |
| auth | Identity, SSO, passkeys. Clerk, WorkOS, Descope, Auth0. |
| storage | Object / blob / file storage. S3-compatible, Tigris, R2. |
| analytics | Product + marketing analytics. PostHog, Plausible, Mixpanel. |
| payments | Stripe, Paddle, LemonSqueezy. |
| cms | Sanity, Contentful, Payload, Hygraph. |
| observability | Logging, tracing, error reporting. Sentry, Datadog, Axiom. |
| ai | LLM / embedding / inference providers. |
| search | Algolia, Typesense, Meilisearch. |
| saas | Everything else. |
pricingModel: one of free, free-tier, paid, usage-based, freemium — or null if unspecified.pricingUrl, freeTierSummary: one-line human-readable pointers so an agent can show the user a sane comparison without scraping.capabilities: array of lower-case tags (e.g. ["postgres", "serverless", "branching"]). Filter with ?capability=… on the HTTP side, or the capability:[…] argument on the MCP tool — multiple values AND together.categories, displayName, description, docsUrl, homepage, npmPackage, inputSchema — all the usual provider metadata.User says "I need a free Postgres database." Agent runs:
list_providers_by_category({
category: "database",
capability: ["postgres"],
pricing: "free-tier"
})
# → { category: "database",
# providers: [
# { id: "neon",
# pricingModel: "free-tier",
# freeTierSummary: "0.5 GB storage and ~190 compute hours per month…",
# capabilities: ["postgres","serverless","branching", ...],
# … }
# ] }
create_signup({
provider: "neon",
input: { name: "my-app-db" }
})database and ai. Use it when a project needs agent-owned memory, records, key-value data, secrets, or hybrid search.# REST discovery
curl https://relay.cumulush.com/v1/index
curl "https://relay.cumulush.com/v1/index/database?capability=agent-memory"
curl "https://relay.cumulush.com/v1/index/ai"
# REST signup
curl -X POST -H "Authorization: Bearer agt_..." \
-H "Content-Type: application/json" \
-d '{
"provider": "cumulus-database",
"input": {
"email": "alex@example.com",
"purpose": "project memory"
}
}' \
https://relay.cumulush.com/v1/signups
# Poll until status is "complete".
curl -H "Authorization: Bearer agt_..." \
https://relay.cumulush.com/v1/signups/<signup_id>
# First complete response only:
{
"signup_id": "<signup_id>",
"status": "complete",
"account_id": "<account_id>",
"initial_credentials": {
"endpoint": "https://db.cumulush.com",
"database_id": "db_...",
"data_token": "cdb_data_...",
"admin_token": "cdb_admin_..."
}
}MCP agents use the same flow with list_categories, list_providers_by_category, create_signup, and get_signup_status. The initial_credentials object is returned exactly once. Put data_token in the project runtime and treat admin_token as a one-time administrative secret.
create_signup({
provider: "cumulus-database",
input: {
email: "alex@example.com",
purpose: "project memory"
}
})
get_signup_status({ signup_id: "<signup_id>" })# "Wire up Postgres and transactional email for this workspace."
curl -X POST -H "Authorization: Bearer agt_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"goal": "Postgres + transactional email for a Next.js app",
"workspaceId": "ws_..."
}' \
https://relay.cumulush.com/v1/intent
# Response (always 200; partial success is normal):
# {
# "resolutions": [
# { "category": "database", "alias": null, "provider": "neon",
# "status": "existing", "accountId": "acc_...",
# "envVar": "DATABASE_URL",
# "revealUrl": "/v1/accounts/acc_.../api-keys/key_.../reveal" },
# { "category": "email", "alias": null, "provider": "resend",
# "status": "provisioning", "signupJobId": "sj_...",
# "pollUrl": "/v1/signups/sj_...",
# "envVar": "RESEND_API_KEY" }
# ],
# "envBlock": "DATABASE_URL=__reveal_required__\nRESEND_API_KEY=__pending__\n",
# "pending": ["sj_..."],
# "unsatisfied": [],
# "unmatchedTerms": [],
# "revealAllUrl": "/v1/accounts/keys/reveal-batch",
# "notes": [
# "Resend signup requires email verification — poll /v1/signups/sj_..."
# ]
# }status: "ambiguous" with a candidates list — pin one to choose.status: "existing" with the accountId — no duplicate provisioning.status: "provisioning" with a signupJobId + pollUrl — poll GET /v1/signups/:id exactly as you would for a direct POST /v1/signups call.__pending__ and __reveal_required__ mark the slots a follow-up call needs to fill. Fresh signups inline plaintext on first read.When you need a specific provider, or two distinct accounts inside the same category (a primary + analytics Postgres, for example), pass a pin array. Each pin becomes its own resolution slot:
{
"goal": "Postgres for the app and a separate Postgres for analytics",
"workspaceId": "ws_...",
"pin": [
{ "category": "database", "providerId": "neon", "alias": "primary" },
{ "category": "database", "providerId": "neon", "alias": "analytics" }
]
}
# → resolutions[0].envVar = "DATABASE_URL_PRIMARY"
# → resolutions[1].envVar = "DATABASE_URL_ANALYTICS"Send Idempotency-Key on retries. Relay caches the response per (agent, key) for 24 hours, so a retry after a 5xx never duplicates signups.
The intent call itself is free. Each spawned signup bills the integrator's quota normally — same gate as a direct POST /v1/signups call. Calling intent on a fully-existing workspace is a free, deterministic dedup check.
The MCP tool is resolve_intent — same shape, slimmer response (no reveal URLs, since LLMs would speculatively call them). For an existing account where the agent needs the actual plaintext, follow up with get_api_key to mint a fresh one.
~/Library/Application Support/Claude/claude_desktop_config.json on macOS (or the equivalent on Windows/Linux) and add the relay server:{
"mcpServers": {
"relay": {
"url": "https://relay.cumulush.com/mcp"
}
}
}Streamable HTTP MCP clients like Claude Desktop, Cursor, and Cody honor the same shape. Restart the client so it picks up the new server.agent_tokenas an argument — Claude Desktop does not forward HTTP headers to MCP tools, so auth lives at the tool layer. Put the token in your agent's system prompt or feed it in the first message:"Use this Relay agent_token for every tool call you make: agt_XXXX…"
User: 'Sign me up for Relay and call me Alex.'
Agent: register_tenant → OTP lands in the user's real inbox → user pastes code → submit_verification_code → agent token minted → whoami confirms.
User: 'I need somewhere to store data for my app.'
Agent: list_categories → sees a 'database' slug → list_providers_by_category(category:'database', capability:['postgres'], pricing:'free-tier') → compares the (now short) list on pricingModel / capabilities / freeTierSummary → create_signup(provider:'<pick>', input:{…}).
User: 'Create a Cumulus account for me.'
Agent: list_providers → create_signup(provider='cumulus-database', input={email:'alex@example.com', purpose:'project memory'}) → get_signup_status loops until complete → returns endpoint, database_id, data_token, and admin_token.
User: 'Confirm the verification email Cumulus just sent.'
Agent: get_signup_status returns status='awaiting_email' → auto_confirm_pending_signup reads inbox, extracts OTP or link, calls the confirm endpoint → workflow resumes → account delivered.
| Tool | Summary | Args |
|---|---|---|
| list_categories | Top-level chunk: which provider categories exist, with counts + alias map. No token required. | — |
| list_providers_by_category | Per-category chunk: full provider details (pricing, capabilities, input schema). Supports capability + pricing filters. | agent_token, category, capability?, pricing? |
| list_providers | List every signup target in one payload (built-in and tenant-defined). Prefer the chunked index above for large catalogs. | agent_token |
| get_provider | Full metadata + JSON Schema input for a single provider. | agent_token, id |
| create_signup | Start a durable signup workflow on the named provider. | agent_token, provider, input |
| get_signup_status | Poll a signup; returns initial_api_key or initial_credentials exactly once on completion. | agent_token, signup_id |
| list_accounts | List the calling user's provisioned accounts. | agent_token |
| get_api_key | Mint a fresh API key on an account (zero-retention; returned once). | agent_token, account_id, label? |
| reveal_api_key | Legacy reveal for keys stored before the zero-retention policy. | agent_token |
| delete_account | Delete an account through the provider's teardown handler. | agent_token |
| register_tenant | Start the Relay bootstrap: email an OTP to the caller. | agent_token |
| submit_verification_code | Complete the bootstrap; returns the first agent token + tenant id. | agent_token |
| register_tenant_product | Register a new signup target on a tenant via MCP. | agent_token |
| get_my_inbox_address | Return the user's agent-readable inbox alias. | agent_token |
| read_inbox | Read recent inbound emails; supports code/link extraction. | agent_token |
| auto_confirm_pending_signup | Poll the inbox for a verification email and auto-resume the workflow. | agent_token |
| share_dashboard_link | Mint a one-time read-only share URL to the dashboard. | agent_token |
| get_subscription_status | Return the tenant's subscription + quota snapshot. During the founding-partner phase the response carries the partnership_status field (sprint_paid / renewed / lapsed) instead of a self-serve plan. | agent_token |
| whoami | Identify the calling agent / user. | agent_token |
| resolve_intent | One-shot goal-to-env resolver. Parses a free-text goal ("Postgres + transactional email"), dedups against existing accounts, kicks signups for the gaps, and returns a paste-ready env block. Deterministic — same goal + same workspace returns the same response. Intent itself is non-billable; sub-signups bill normally. | agent_token, goal, workspace_id, [pin] |
Full schemas are published on the MCP endpoint via tools/list. Browse the same shapes as REST at /docs/api.
CLAUDE.mdso every future AI session re-uses it without re-sending an OTP to the user's inbox.## Relay RELAY_AGENT_TOKEN=agt_XXXXXXXX… # Expires: 2026-05-21 (30 days from 2026-04-21). # This token lets your AI agent provision SaaS accounts via Relay. # Re-run register_tenant in MCP after expiry; or pass never_expires:true # if the user explicitly asked for a non-rotating token.
// 1. Hand the user a Stripe Checkout link
start_subscription({ agent_token, tenant_id, plan: "builder" })
// → { checkout_url: "https://checkout.stripe.com/c/pay/cs_..." }
// 2. Human opens the link, pays with a card, is redirected back.
// 3. Stripe's webhook flips the subscription to active; poll:
get_subscription_status({ agent_token, tenant_id })
// → { status: "active", plan: "builder", quota: {…} }If the tenant is already subscribed, start_subscription returns a Stripe Billing Portal URL under already_active.portal_url so the user can change plan, update their card, or cancel — no duplicate subscription.
When a user mints an agent token, it's pinned to whichever workspace they were viewing at the time. That token can only see the data inside that workspace. You never need to include a workspace id in your calls — the binding is on the token row. A call to GET /v1/user/accounts with a token minted inside Workspace A returns Workspace A accounts; calling with a token minted inside Workspace B returns Workspace B accounts. Cross-workspace access is not possible.
# List the caller's workspaces (active marker included).
GET /v1/user/workspaces
# Create a new workspace. Optional { make_active: true } flips the session.
POST /v1/user/workspaces { name, slug?, make_active? }
# Rename.
POST /v1/user/workspaces/:id/rename { name }
# Hard-delete (requires confirm_name to equal the workspace name).
DELETE /v1/user/workspaces/:id { confirm_name }
# Cookie-session-only: pick a different active workspace.
POST /v1/user/workspaces/:id/switchThe switchendpoint is cookie-only because bearer tokens carry an immutable workspace pin. A bearer can still create + delete + rename workspaces on the user's account, but it can never make itself scope to a different workspace.
signup and delete_accountbills one action against the integrator's monthly quota. Key-lifecycle actions (get_api_key, reveal_api_key, rotate, revoke) are debounced per (end-user, integrator, provider) per UTC day: the first action of the day on that triple bills one action; every later same-day action on the same triple is free against the integrator quota. This means it's safe — and recommended — for an agent to retry reveals after a transient failure, rotate keys right after a deploy, or call into the same provider repeatedly for one user without inflating their integrator's bill. Per-end-user abuse caps still apply on top so a runaway loop cannot hide behind the debounce.