API reference
The TellWang control-plane API manages Woks and resources; each Wok also exposes its own data API (PostgREST over your tables, auth, storage). Everything Wang and the dashboard do is available here, so automation and CI use the exact same surface.
Base URL & versioning
The control-plane API is versioned in the path; breaking changes ship under a new version, never in place.
https://api.tellwang.com/v1 # control plane (Woks, resources)
https://<wok-id>.tellwang.com/rest/v1 # a Wok's data APIAuthentication
Authenticate with a bearer token: an API key or service-account token for the control plane; the Wok's anon or service-role key for its data API. Every request is authorized server-side against the caller's org/project scope.
curl https://api.tellwang.com/v1/woks \
-H "Authorization: Bearer <api-key>"Conventions
- Idempotency: send an
Idempotency-Keyheader on writes; retries are safe and never double-apply (or double-charge). - Errors: a stable envelope —
{ "error": { "code", "message", "hint", "correlation_id" } }.codeis the machine-readable identifier (classify on this);messageis human-prose;hintis the remediation (sometimes omitted on terse errors);correlation_idis always present — include it when filing a ticket. See Error codes for the 25 most-hit codes with per-code remediation. - Pagination: cursor-based via
?cursor=+limit; responses includenext_cursor. - Rate limits: per-token;
429withRetry-After.
Example — provision a Wok
A newly provisioned Wok includes the standard reference app and GEO foundation. The private model combines a bounded live-homepage snapshot with the owner's topics to plan language-specific keywords, with deterministic topic queries as the fail-safe.
curl -X POST https://api.tellwang.com/v1/woks \
-H "Authorization: Bearer <api-key>" \
-H "Idempotency-Key: 7f3a-..." \
-d '{ "name": "acme-pos" }'
# → 201 { "id": "wok_abc123", "url": "https://wok_abc123.tellwang.com",
# "anon_key": "...", "state": "ready" }Diagnostics
Three layers, from cheapest to most detailed. All return JSON.
GET /v1/healthz— liveness only.200 {"status":"ok"}when the cp process is running and able to serve. Doesn't say anything about the underlying systems. Use this for load-balancer health checks and CI smoke tests; sub-millisecond.GET /v1/diag— platform semantic health. Walks every subsystem (docker daemon, Caddy, waitlist DB, audit-log chain, image cache, etc.) and reports per-check status + evidence:
Use this when you want to know "is anything wrong, and what is it." Open (no auth) so the platform's status is queryable from outside. Top-level{ "status": "ok", "correlation_id": "4aaebbc7a83773d9", "checks": [ {"name":"swarm", "status":"ok", "detail":"docker daemon reachable"}, {"name":"caddy", "status":"ok", "detail":"running + Caddyfile valid"}, {"name":"cp_db", "status":"ok", "detail":"pgxpool acquire 1.2ms"}, {"name":"audit_log", "status":"ok", "detail":"hash chain intact at id=42117"}, ... ] }statusis the aggregate acrosschecks(ok / degraded / down).GET /v1/diag/wok/{id}— per-wok semantic health. Same shape, but checks scoped to one wok's stack: every container's running-state, postgres readiness, port reachability, replication slot health. Bearer-of-owning-org (or operator-bootstrap); cross-org returns 404 with no leak. Use this in your customer's own dashboards or to debug "my wok stopped working."
The semantic distinction: /healthz answers "should the load balancer keep sending us traffic"; /diag answers "what's right and what's broken, with evidence." TellWang's automated health monitoring reads /diag as its primary signal.
Wok lifecycle
Provision returns the canonical row; everything below operates against an existing wok. Bearer-of-owning-org on all of them; cross-org returns 404.
GET /v1/woks/{id}— single row:{id, host, url, status, created_at, updated_at}. Useful for "is this wok still mine + alive."GET /v1/woks— list active woks for the calling org.GET /v1/woks/{id}/connection— the wiring sheet. Returns everything@supabase/supabase-jsneeds:
Every wok is reachable at{ "wok_id": "acme-pos", "status": "ready", "url": "https://acme-pos.tellwang.com", // base URL for supabase-js "rest_url": "https://acme-pos.tellwang.com/rest/v1", "anon_key": "eyJhbGciOi...", // client-side JWT, RLS-bound "service_role_key": "eyJhbGciOi...", // SERVER-SIDE ONLY; bypasses RLS "functions_url": "https://acme-pos.tellwang.com/functions/v1", "realtime_url": "https://acme-pos.tellwang.com/realtime/v1", "storage_url": "https://acme-pos.tellwang.com/storage/v1", "auth_url": "https://acme-pos.tellwang.com/auth/v1", "rag_url": "https://acme-pos.tellwang.com/rag/v1", // only when enabled "redis_url": "redis://default:<dbpass>@redis:6379" // internal-only, edge fn use }https://<wok-id>.tellwang.comas the supabase-js base URL, with the 5 standard Supabase API path prefixes (/rest/v1,/auth/v1,/realtime/v1,/storage/v1,/functions/v1) routing transparently to the wok's services. Edge functions that returntext/event-streamare proxied unbuffered for SSE, and WebSocket Upgrade handshakes pass through the same functions route. Caddy fetches a Let's Encrypt cert on the first HTTPS hit via on-demand TLS (no pre-provisioning step). Keys are minted on-the-fly from the wok's per-instance JWT secret; the secret never leaves the cp.service_role_keybypasses RLS and must never reach a browser — wire it only into server-side processes (edge functions, your backend, CI).redis_urlis internal-only (no external access); edge functions read it asprocess.env.REDIS_URL, see Functions → Redis + BullMQ.POST /v1/woks/{id}/restart— graceful restart of every container in the wok's stack (postgres → gotrue → postgrest → realtime → storage → functions). Picks up env-var changes that PUT/POST/envwrote since the last restart. Idempotent (returns the same shape regardless of pre-state).POST /v1/woks/{id}/resume— re-attach asuspendedwok (free-tier inactivity hibernation). 200 if alreadyready; 409 if already destroyed.DELETE /v1/woks/{id}— tear down the stack + volumes and mark it destroyed. Deleting a production Wok also removes its linked staging Wok, so no preview stack is orphaned. Control-plane environment variables and secrets remain until the separate purge step; export anything you need before deleting.
Managed RAG
RAG is an optional per-Wok API and durable worker, not another application contract to learn. Choose LangChain or LlamaIndex when enabling it; apps always call the same /rag/v1 surface. Vectors stay in the Wok's Postgres through pgvector, source and generated files stay in private Wok Storage, and jobs use the Wok's Redis queue. The gateway accepts only anon, authenticated, and service_role Wok tokens. Trusted application roles and groups come from signed app_metadata, but application roles cannot impersonate those reserved Wok roles; anonymous callers can retrieve only material marked public.
Owners do not need to call these endpoints by hand. Open an app's Documents resource in the dashboard to set up the sidecar, create a governed collection, upload and index large selections in sequential count-and-byte-bounded batches, inspect progress, apply a hard Do not use rule, block selected files from one support agent without removing them from internal work, create a new file from selected sources, or run an insight across the collection. Large inventories load 500 rows at a time; active agents and generated files remain visible after refresh. The org-level Knowledge editor is different: it holds a small plain-text memory for Wang itself, while app Documents is the governed vector corpus for application features.
PUT /v1/woks/{id}/ragwith{"framework":"llamaindex","profile":"standard"}— enable and wait for readiness. Framework may bellamaindexorlangchain; profile may bestandardorhigh-throughput.POST /v1/woks/{id}/rag/collections— create a collection with a stable Wok-unique slug and baseline policy. The slug can be fixed before execution for exact Wang evidence; the generated UUID remains the data-plane reference. Exact retries are idempotent, while reusing the slug for different settings is a conflict. Documents can allow signed principals, roles, or groups; deny named principals; set public/internal/confidential/restricted sensitivity; or setexcluded:true.GET /v1/woks/{id}/rag/collectionsandGET /v1/woks/{id}/rag/collections/{collection_id}/documents— inspect active revisions, document/chunk counts, source versions, effective access policies, status, and errors with bounded cursor pagination.POST /v1/woks/{id}/rag/collections/{collection_id}/documents/{document_id}/exclude— quarantine an indexed document without downloading it again. The completed job atomically activates an excluded zero-chunk revision and purges its prior RAG text and vectors; its private source file remains in Storage until separately deleted.POST /v1/woks/{id}/rag/ingestions— submit up to 1,000 existing Storage objects per atomic batch. Successful later batches extend or update the active collection, so 1,000 is not a collection-size cap. The prior index remains live until the complete batch succeeds. Promotion retains one active membership snapshot plus compact revision metadata, while a failed batch discards candidate-only vectors. Excluded objects are never downloaded or embedded; excluded text ranges are removed before chunking. Promoting anexcluded:trueobject purges its older RAG text/vector revisions; delete the original Storage object separately if the source file must also be erased.POST /v1/woks/{id}/rag/agents— create a grounded customer-service agent.excluded_document_idscan block up to 500 active files for this agent only while keeping them available to other agents, generation, and insights. End-user chat uses the Wok JWT at{rag_url}/agents/{slug}/chat; access is filtered before any model call, then every returned claim is checked against only its cited passages. An unsupported claim or malformed check returns the configured fallback. Anonymous chat is off unlessallow_anonymous:true, and even then only public passages are eligible.GET /v1/woks/{id}/rag/agents— list enabled agents with their exact corpora, excluded file IDs and stable paths, grounding policy, exposure, instructions, and escalation settings.GET /v1/woks/{id}/rag/agents/{slug}— inspect the enabled agent's collections, excluded file IDs/paths, and exact grounding, citation, anonymous-access, instructions, and escalation settings. Runtime v2 evidence also pins the expected collection set, source-path block list, and exposure setting.DELETE /v1/woks/{id}/rag/agents/{slug}— turn an agent off immediately. Re-saving the same slug re-enables it.POST /v1/woks/{id}/rag/generations— create Markdown, DOCX, PDF, CSV, or JSON drafts from an explicit non-empty document set. Every named file must exist and be readable, otherwise no partial draft is produced. Drafts stay under privaterag-results/generated/, inherit the strictest common source access, and default to a 50-call model budget (maximum 200). Direct app callers need signedrag_authorrole orrag-authorsgroup.POST /v1/woks/{id}/rag/insights— queue summary, comparison, classification, extraction, clustering, or trend work. Optional JSON Schema output must validate before success. A caller-selectedmax_model_callsis capped at 500 to prevent runaway corpus-wide spend; direct app callers need RAG author access.POST /v1/woks/{id}/rag/interview-projects— create a durable, industry-neutral Interview Processing project with a strict extraction schema, governed collections, and confidence threshold.POST /v1/woks/{id}/rag/interview-projects/{project_id}/process— process up to 100 selected interview documents as separate durable jobs. Every populated field needs an exact source quote; weak or incomplete evidence enters review instead of a report.GET /v1/woks/{id}/rag/interview-projects/{project_id}/recordsandPATCH /v1/woks/{id}/rag/interview-projects/{project_id}/records/{record_id}— inspect evidence and confidence, then approve, reject, or correct records against the same schema. Reviewer decisions are audited.POST /v1/woks/{id}/rag/interview-projects/{project_id}/reports— generate a private DOCX, PDF, or XLSX from approved records only. Counts and missingness are deterministic; qualitative findings cite approved record IDs and pass another support check.GET /v1/woks/{id}/rag/jobs/{job_id}— poll asynchronous work. For ingestion, only parent-job success means the new collection revision is active.GET /v1/woks/{id}/rag/jobs/by-key?kind=…&idempotency_key=…— resolve a parent job from the immutable key chosen before execution, including for Wang Runtime v2 evidence.GET /v1/woks/{id}/rag/jobs/{job_id}/items— inspect cursor-paginated per-file outcomes and errors for a large ingestion or Interview Processing batch.GET /v1/woks/{id}/rag/artifacts— list generated files and insight outputs durably with provenance and inherited policy.GET /v1/woks/{id}/rag/artifacts/{artifact_id}— policy-check and mint a 15-minute signed URL for a generated file.
Use Wok Storage's resumable upload flow before the manifest endpoint for very large corpora. The multipart /rag/uploads convenience route is capped at 100 files; the dashboard also keeps each sequential batch below 900 MiB so multipart overhead stays within the management proxy ceiling. Files use digest-bound content-addressed paths, so identical retries do not upload again and changed bytes cannot silently replace indexed content; the manifest route remains the scalable contract. Disabling RAG closes its route and revokes its dedicated model key while keeping the corpus in the Wok for re-enable.
Audit log
Every state-changing call to the cp writes a row to a tamper-evident, hash-chained audit log. Customers query their own org's chain:
GET /v1/audit-log— list entries for the calling org, newest-first, cursor-paginated. Query params:limit— 1..200 (default 100).cursor— opaque cursor returned asnext_cursorfrom the previous page.action— exact match on action string, e.g.wok.exec_sql,wok.env.set,wok.frontend.deploy.actor_kind— one ofoperator|bearer|internal|system.target_kind— e.g.wok,llm_key,email_domain.since/until— RFC3339 timestamps.
Response shape: {entries:[{id, actor_kind, actor_id, action, target_kind, target_id, http_status, correlation_id, payload, created_at}], next_cursor}. Payload values that touch secrets (env values, key material) are stored as fingerprints — names + hashes only, never plaintext.
The dashboard's Activity card (under YOUR WOKS) hits this endpoint with default filters. For programmatic access, the same Bearer + same shape works from any client.
Web analytics & session replay
Apps with first-party analytics enabled report cookieless pageviews, daily visitors, custom events, aggregate click cells, and scroll depth. GET /v1/woks/{id}/analytics/web?days=7 returns traffic and sources; GET /v1/woks/{id}/analytics/heatmap?path=/&days=7 returns a normalized 20×30 click grid and ten scroll-depth buckets. Raw click coordinates, selectors, and DOM text do not enter these aggregate datasets.
Session replay is separate and off by default. An owner or admin enables it with PATCH /v1/woks/{id}/analytics/session-replay and {"enabled":true,"acknowledge_privacy":true}. The response reports whether platform sampling is active, its rate, and the seven-day retention. Recordings mask all text and input values, omit canvas and fonts, and exclude subtrees marked data-tw-block. Each upload is signed for one Wok and one unlinkable session. Public bootstrap and ingest are rate-limited; ingest redacts text-like attributes, event handlers, and URL queries/fragments again before owner-only storage capped per session, per Wok, and globally.
GET /v1/woks/{id}/analytics/sessions— recent recording metadata. Requires the build capability.GET /v1/woks/{id}/analytics/sessions/{sid}— masked rrweb events for the dashboard player. Requires the build capability.PATCH /v1/woks/{id}/analytics/session-replay— enable or disable recording for the app. Owner/admin only; enabling requires the privacy acknowledgement.
Wok internals — SQL, functions, realtime, scheduler, env
Once a Wok is provisioned, drive its full surface from the same Bearer-of-owning-org auth. Every endpoint is authorized server-side against the owning org — cross-org access returns 404 with no leak. All available from the dashboard's per-wok widgets too.
SQL console
POST /v1/woks/{id}/exec_sql— body{"sql":"<your-sql>", "no_pgrst_reload":false}. Runs in your wok's db container viapsql; returns{stdout, stderr, exit_code}. Auto-appendsNOTIFY pgrst, 'reload schema'on success so PostgREST picks up new tables immediately (opt-out viano_pgrst_reload). Body cap 1 MiB; stdout/stderr capped at 1 MiB.
Edge functions
POST /v1/woks/{id}/functions/{name}— body{"code":"<index.ts>","files":{"lib/helper.ts":"<source>"}}.filesis optional and contains local modules relative to the function directory; the complete tree is capped at 256 KiB. Name^[a-z][a-z0-9-]{0,30}$. Returns 201 with the row +invoke_urlyou can curl immediately. 503CP_FUNCTIONS_NOT_ENABLEDif the Wok predates Functions support; the reconciler heals it on the next sweep.GET /v1/woks/{id}/functions— lists each fn as{name, code_bytes, code_sha256, module_count, invoke_url, created_at, updated_at}(omits source bodies).GET /v1/woks/{id}/functions/{name}— returnscodeplus the localfilesmap so you can pull the complete deployed function.DELETE /v1/woks/{id}/functions/{name}— idempotent; missing returns 204.POST /v1/woks/{id}/triggers· body{table, event, function_name}— wires a DB trigger thatpg_notifys on INSERT/UPDATE/DELETE and dispatches to your function.
Functions receive the following env vars at runtime: SUPABASE_URL (the wok's PostgREST), SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, JWT_SECRET (the stack's JWT signing secret — the same one GoTrue signs with and PostgREST validates against), SUPABASE_DB_URL, REDIS_URL (per-wok Redis 7 container, AUTH via the wok's db password). These are injected and platform-managed — set_env rejects them; read them with Deno.env.get(...). The Redis instance is bullmq-ready out of the box — see Functions → Redis + BullMQ for the canonical pattern.
Release receipt
GET /v1/woks/{id}/release answers what is live without reconstructing it from several tools. It returns the newest attempt, the current successful release, the previous rollback target, the Wok Git source commit, the shared release manifest, a SHA-256 digest of the deployed bytes, changed runtime surfaces, verification evidence, and publish/rollback approval identity. A newly activated deployment remains verifying; it cannot become succeeded until every required probe passes. The current release follows runtime activation order, so a delayed probe cannot make an older candidate replace newer live bytes in the receipt.
release_status=verified means the exact frontend tree, complete function-module tree, ordered tenant migration checksums, Wok Git source, and runtime artifact all match the manifest, and every changed surface has relevant passing evidence. Frontend/SSR releases need a browser or public-URL check, function releases need a function invoke, and database releases need their declared postconditions. A failed required probe marks the attempt failed and restores the latest previously succeeded code release. A browser check counts only for this Wok's standard hostname or a verified custom domain, so checking an unrelated public page cannot verify the release. External remote URLs are credential-scrubbed and compared with their last fetched tracking ref; Wok Git remains authoritative.
POST /v1/woks/{id}/releases/{deployment_id}/rollback creates a new audited rollback release from a source-linked successful deployment. It restores frontend and functions exactly, including removing functions absent from the target. It never deletes history or reverses schema/data. If redeployment fails midway, TellWang replays the pre-attempt source as compensation so moving Git alone cannot leave partially rolled-back code live.
Realtime publication
PUT /v1/woks/{id}/realtime/tables/{schema}/{name}— adds the table to thesupabase_realtimepublication. Idempotent.GET /v1/woks/{id}/realtime/tables— returns{tables:[{schema, table}], publication}.DELETE /v1/woks/{id}/realtime/tables/{schema}/{name}— drops the table from the publication.GET /v1/woks/{id}/realtime/status— reports wal_level + slot health.
Scheduler (pg_cron)
POST /v1/woks/{id}/scheduler/jobs— body{name, schedule, command}. Schedule = cron expression (0 3 * * *) or interval (30 seconds). Command = any SQL, runs inside your wok's db. Returns 201 with the cron job_id.GET /v1/woks/{id}/scheduler/jobs— lists each as{job_id, name, schedule, command, active}.DELETE /v1/woks/{id}/scheduler/jobs/{name}— unschedule. Idempotent.
Environment variables
PUT /v1/woks/{id}/env/{name}— body{value}. Name^[A-Z][A-Z0-9_]{0,63}$. Encrypted at rest under the cp's KEK (AES-256-GCM envelope). 204 on success. Wired onto the wok's GoTrue + Functions containers; cp restarts them on next mutation.POST /v1/woks/{id}/env— bulk import. Body is one of{"vars":{"NAME1":"v1","NAME2":"v2"}}or{"dotenv":"NAME1=v1\nNAME2=v2"}. Up to 200 names per call; all names validated against the regex BEFORE any write (atomic — one bad name fails the whole batch with the offender list in the error envelope). Single pgx transaction so a network failure mid-batch never leaves a half-applied state. Dotenv parser tolerates# comments,export NAME=valshell-prefix, and matching surrounding single/double quotes. Does NOT auto-restart the wok stack — POST/restartafterward if you need the new values live immediately (the per-var PUT path auto-restarts, the bulk path doesn't because a 20-var batch otherwise burns ~30s on container churn).GET /v1/woks/{id}/env— returns{vars:[{name, created_at, updated_at}]}. Values are NEVER returned (write-only).DELETE /v1/woks/{id}/env/{name}— unset. Idempotent.
Most common use: wire OAuth providers (Google, GitHub, Apple, Azure, Bitbucket, Discord, Facebook, LinkedIn, Notion, Slack, Spotify, Twitch, Twitter, Zoom) by setting GOTRUE_EXTERNAL_<PROVIDER>_ENABLED=true + _CLIENT_ID + _SECRET + _REDIRECT_URI. See Auth → OAuth providers.
curl -X POST https://tellwang.com/v1/woks/$WOK/env \
-H "Authorization: Bearer $TELLWANG_KEY" \
-H "Content-Type: application/json" \
-d '{"dotenv":"GOTRUE_EXTERNAL_GOOGLE_ENABLED=true\nGOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=...\nGOTRUE_EXTERNAL_GOOGLE_SECRET=...\nGOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=https://..."}'
curl -X POST https://tellwang.com/v1/woks/$WOK/restart \
-H "Authorization: Bearer $TELLWANG_KEY"Account, plans & model gateway
The control plane runs the customer onramp, plan tiers, and a metered model gateway — all under the same Bearer key you provision Woks with.
POST /v1/signup·POST /v1/login— self-serve account + org + first API key from an email and password. Rate-limited. Open by default; where the deployment is invite-gated, signup also takes aninvite_code.GET /v1/plans— the catalog (free / pro / scale). Open (no auth). Response:{plans:[{plan, wok_quota, llm_tokens_grant, llm_requests_grant, price_cents_month, default_rest_replicas, default_standby}]}. Limits are free=1 app, pro=3, scale=25; each app includes a production Wok and linked staging Wok. Pro/scale production Woks start with two REST replicas and a DB standby when capacity is available.GET /v1/orgs/{slug}/plan— your plan, its entitlements, and Woks used against the limit.POST /v1/llm-keys— mint a model-gateway key; the grant defaults to (and is capped at) your plan.GETlists key balances and the monthly free grant left,PATCH /v1/llm-keys/{prefix}switches the key model,/topuprefills toward the plan ceiling,/checkoutbuys beyond it,DELETErevokes. Point any OpenAI-compatible client at/v1/llm/v1/chat/completions. Passstream: truefor SSE streaming responses (see Streaming) — token metering happens after the stream closes, off the finalusagechunk.GET /v1/llm-keys/{prefix}/billing— the Stripe-credit ledger for a key, to reconcile a purchase against the meter.POST /v1/orgs/{slug}/email/domain— attach a sending domain (e.g.mail.acme.com). Returns the DKIM / SPF / MX records to publish, or auto-publishes them when TellWang manages the apex zone.GET /v1/orgs/{slug}/email/domainspolls live status;DELETErevokes. See Email → Bring your own domain.POST /v1/orgs/{slug}/billing/checkout/subscription— buy aproorscalesubscription via Stripe Checkout. Body{plan, success_url, cancel_url}returns a Stripe-hosted checkout URL. On payment, Stripe'scustomer.subscription.createdwebhook flips your org plan +wok_quota; cancellation drops back tofree.GET /v1/orgs/{slug}/billing/subscriptionreads the current status.
Plan changes are self-serve via the subscription checkout above (the operator-only PUT /v1/orgs/{slug}/plan still exists for overrides); per-org quota overrides remain operator-gated. Operators can backfill plan runtime defaults for existing Woks with POST /v1/orgs/{slug}/runtime-defaults.
Prepaid credits
Top up once, debit silently per action. One USD-cents balance per org covers domain registrations today and any future per-call charge. Two top-up rails — Stripe Checkout (card) or Solana USDC (crypto). After the initial deposit there's no second click per buy.
GET /v1/orgs/{slug}/credits— current balance, recent ledger entries, both rails' status.POST /v1/orgs/{slug}/credits/topup— body{amount_usd, success_url?, cancel_url?}. Returns a Stripe Checkout URL; on payment the balance lands within ~5s via the same webhook plumbing the LLM top-up uses. Per-session cap $5000.GET /v1/orgs/{slug}/credits/crypto-deposit— returns the operator's Solana USDC token account address + the per-org memo string (tw:org:<slug>). Send any USDC amount with the memo attached; the watcher credits ~15-45s after confirmation. Idempotent on tx signature.
On any paid action with an insufficient balance, the response is 402 {status:"insufficient_credits", balance_cents, deficit_cents, top_up_url} — the top_up_url is a pre-built Stripe Checkout covering exactly the deficit, so the agent or dashboard hands the customer one click to unblock the buy.
Domain registration
Buy a domain through the Cloudflare Registrar with TellWang as the trustee. Dynamic pricing: Cloudflare's at-cost × 1.30 with a $5 USD floor; pre-flight failures (invalid syntax, unsupported TLD, name unavailable, cap exceeded) are FREE — no Stripe call until the buy succeeds. Supported TLDs cover the standard set (.com .net .org .dev .app .xyz .info .biz .pro .site .online .store .tech .blog .page .click .link .live .world) and a premium tier (.io .ai .co .me .tv .gg) when the CF Registrar API supports them.
POST /v1/orgs/{slug}/domains/check— body{names:[], expected_max_price?}. Returns a per-name quote:{ok, name, tier, at_cost_usd, billed_usd, reason?}. Always check before buy so you know what you'll be charged. Max 20 names per call.POST /v1/orgs/{slug}/domains/buy— body{name, expected_max_price?, auto_renew?, privacy?, registrant_contact?}. Silent debit from the org's prepaid credit balance, then registers via CF, then upserts the row. Returns{status:"active"|"pending"|"failed", domain_row, debit_cents, balance_remaining}. On insufficient credits returns 402 withtop_up_url(see above). On CF-register failure post-debit, the credit is auto-refunded.POST /v1/orgs/{slug}/domains/attach— body{domain}. Attach a domain already in the operator's CF account (no registrar round-trip) so the org can manage its DNS without a fresh registration.GET /v1/orgs/{slug}/domains— list owned domains.GET /v1/orgs/{slug}/domains/{name}— one domain's local row + a live CF re-fetch (auto-syncs the zone_id when CF surfaces one we don't have yet).GET /v1/orgs/{slug}/domains/{name}/dns— list zone records.POST /v1/orgs/{slug}/domains/{name}/dns— body{type, name, content, ttl?, proxied?, priority?, comment?}. Create A / AAAA / CNAME / MX / NS / TXT / CAA / SRV / PTR / DS / HTTPS / SVCB records.PATCH /v1/orgs/{slug}/domains/{name}/dns/{id}— update record.DELETE /v1/orgs/{slug}/domains/{name}/dns/{id}— delete record.
Wang — the customer-facing agent
Wang is the agent layer described on How Wang works. Reach it two ways today:
- In-dashboard chat — open tellwang.com/dashboard, the "Wang" card auto-renders after sign-in. First message mints a chat key automatically. Talks to
POST /v1/llm/v1/chat/completionswith the Wang persona. No setup, no extra deploy. - Channels — connect a Telegram or Slack bot in the dashboard's Channels tab and your team talks to Wang from the chat app they already use. See Channels below.
Wang chat persistence
The in-dashboard Wang chat keeps the conversation on the server too, so logging in from a fresh browser brings the thread back. Schema mirrors the OpenAI chat-completions wire format so rows round-trip without lossy re-mapping.
GET /v1/orgs/{slug}/wang/chat— returns the last 200 messages oldest-first as{messages:[{id, role, content?, tool_calls_json?, tool_call_id?, tool_name?, created_at}]}. Role is one ofuser|assistant|tool.POST /v1/orgs/{slug}/wang/chat— append one. Body matches the OpenAI message shape:{role, content?, tool_calls?, tool_call_id?, name?}. Content cap 200 KiB. Returns 201 with the inserted row.DELETE /v1/orgs/{slug}/wang/chat— clear all messages for the org. Returns 204.
No per-call billing — chat history is metadata, not a paid action; LLM tokens are already metered on the sllm_ key.
Org instructions & knowledge base
Teach Wang about your business so it answers with your context instead of generic defaults. Both Wang surfaces — the in-dashboard chat and the Telegram/Slack channels — honor them.
- Instructions — a markdown blob Wang always follows (tone, naming conventions, approval policy, "always provision in region X").
GET / PUT / DELETE /v1/orgs/{slug}/wang/instructions(PUT body{body}, 20 KB cap). Appended to Wang's base persona; it never overrides the safety / boundary-authz rules. Edit it in the dashboard's Wang tab. - Knowledge base — documents Wang retrieves per question.
GET /v1/orgs/{slug}/wang/knowledge(list, bodies omitted),POST(add{title, body}; 100 KB/doc, 200 docs/org),DELETE …/wang/knowledge/{id}, andGET | POST …/wang/knowledge/search?q=(full-text-ranked top matches). Retrieved per-query via thesearch_knowledgeMCP tool (any MCP client) and client-side by the dashboard chat.
Retrieval is Postgres full-text search today; semantic (embedding) retrieval is a roadmap upgrade.
Inbound email — Wang can read it
Mail received on a domain attached to a Wok lands in that Wok's inbox; Wang can list and read it to summarize, extract, classify, or draft a reply.
GET /v1/woks/{id}/inbox— list received messages (bodies omitted;{id, from, to, subject, received_at}).GET …/inbox/{message_id}— fullbody_text+body_html.DELETE …/inbox/{message_id}— idempotent.- Wang reaches these through the
list_inbox+read_emailMCP tools. Delivery is the Resend inbound webhook (POST /v1/webhooks/email/inbound, Svix-signed) routed to the Wok by recipient domain.
Channels — Telegram & Slack
Connect a messaging bot so your team talks to Wang where they already are. The bot credential is yours (the @BotFather token / Slack bot token), stored encrypted; no platform-wide bot is involved. Inbound messages run a Wang turn grounded in this org's instructions + knowledge base, and the answer is posted back to the same chat.
GET /v1/orgs/{slug}/channels— list connected channels (kind,token_fingerprint,webhook_url; never the token).PUT /v1/orgs/{slug}/channels/{kind}— connect/rotate.kindistelegramorslack. Body{token, signing_secret?}(signing_secretrequired for Slack). For Telegram we callsetWebhookfor you and return the bot@username; for Slack we return thewebhook_urlto set as the Event Subscriptions Request URL.DELETE /v1/orgs/{slug}/channels/{kind}— disconnect (TelegramdeleteWebhookfirst), 204.- Inbound (provider-called, public):
POST /v1/channels/telegram/{secret}andPOST /v1/channels/slack/{secret}. Authenticated by the random per-channel secret in the path, plus Telegram'sX-Telegram-Bot-Api-Secret-Tokenheader / Slack's HMAC request-signature.
Connect it from the dashboard's Channels tab, or via the connect_channel / list_channels / disconnect_channel MCP tools. v0 answers from a channel are LLM + knowledge-base grounded (great for support/Q&A); having Wang run its full tool-loop in response to a channel message is a roadmap upgrade.
Key management
Generate and manage encryption keys, then encrypt, decrypt, and sign through your Wok — the private key material never leaves the control plane. Key types: aes-256-gcm (encrypt/decrypt), ed25519 and rsa-4096 (sign/verify). An ed25519 key's public key is a Solana address, so /sign is a remote signer for Solana transactions. Bearer-of-org; use $ORG_SLUG or me. See Key Management for worked examples.
POST /v1/orgs/{slug}/kms/keys— create{name, type}. Returns metadata only (name, type, version, fingerprint, public key for asymmetric); the material is never returned.GET /v1/orgs/{slug}/kms/keyslists;GET …/kms/keys/{name}reads one;DELETEis idempotent.POST /v1/orgs/{slug}/kms/keys/{name}/rotate— new version becomes current; older versions are retained so prior ciphertext and signatures still resolve.POST /v1/orgs/{slug}/kms/keys/{name}/import— bring your own key.aes-256-gcm:{material_b64}(32 bytes).ed25519:{secret_key}(asolana-keygenid.json array or a base58 secret key) or{seed_b64}.rsa-4096:{private_key_pem}. The response echoes the derived address so you can confirm it.PUT /v1/orgs/{slug}/kms/keys/{name}/state—{state}enable or disable a key.POST …/kms/keys/{name}/encrypt—{plaintext_b64, aad_b64?}→{ciphertext}.…/decryptreverses it.…/generate-data-keyreturns a fresh data key plus its wrapped form for envelope-encrypting large payloads locally.POST …/kms/keys/{name}/sign—{message_b64}→{signature, signature_b64, version}(theed25519signature is base58, ready for a Solana transaction).…/verifytakes{message_b64, signature}→{valid}.
Model gateway wire format
The model gateway is OpenAI-compatible. Any library that targets the OpenAI HTTP API works against https://tellwang.com/v1/llm/v1 with an sllm_ Bearer.
POST /v1/llm/v1/chat/completions— proxy to TellWang's private VLM. Usemodel:"private"; text andimage_urlcontent blocks are supported.- Streaming — pass
stream: true. The gateway returns SSE (Content-Type: text/event-stream) and force-injectsstream_options.include_usage = trueon the upstream so the finaldata: {"usage":{...}}chunk arrives for post-stream meter debit.X-Accel-Buffering: nosuppresses Caddy buffering so chunks land at upstream latency. Client disconnect mid-stream still debits the tokens already delivered. GET /v1/llm/v1/models— OpenAI-spec discovery with the singleprivatemodel.