# Changelog (/docs/changelog) Stay up to date with the latest changes across the Voidnet ecosystem. ### 2026-09-29 — Model picker moves to where you type *new · product · voidai* The model picker moved out of the top header and into the message bar — open the reasoning button to search and switch models without leaving the composer. ### 2026-09-26 — Draw on images, catch trending topics *new · product · voidai* Sketch directly on an attached image before sending it, and tap live trending topics under the composer to search what is happening now in your country. ### 2026-09-24 — A canvas for writing: essays, letters, emails *new · product · voidai* Ask VoidAI to write something you keep — an essay, story, letter, or email — and it lands in a dedicated canvas with a recipient line for emails and fill-in fields for names and details. ### 2026-09-23 — VoidAI now sees your workspace images *new · product · voidai* Ask about an image saved in your workspace and VoidAI describes what is actually in it — diagrams, screenshots, photos. Text-only models say so instead of guessing. ### 2026-09-23 — CLI 0.1.1: safer logins for scripts and automation *fix · product · voidnet* Pipe your token with voidnet login --with-token so it never lands in shell history, unknown flags now fail with usage instead of being ignored, and logout keeps your local token when the server cannot be reached. ### 2026-09-23 — Compatibility promise: 30-day notice on breaking changes *new · platform · voidnet* Breaking changes to publisher-facing APIs are announced in this changelog at least 30 days in advance, with the current version kept routing until the window closes. Non-breaking fixes need no notice. ### 2026-09-22 — Docs pages now serve plain markdown for agents *new · docs · voidnet* Every docs page has a matching .md URL, so AI assistants read clean markdown instead of scraping pages. ### 2026-09-22 — Voidnet CLI 0.1.0: your terminal for Voidnet *new · product · voidnet* Install with npm install -g openvoidnet. Script everything with JSON output, check service health, stay updated automatically — and sign in from your browser, with per-app machine tokens managed in Console. ### 2026-09-21 — Wallet payments apply exactly once *fix · billing · voidnet* Retrying a wallet top-up or grant can no longer charge or credit twice — a retry finishes the same payment instead of duplicating it. ### 2026-09-21 — Model picker tidied: DeepSeek Flash leads *change · product · voidai* Retired model names are gone and Groq chat models leave the picker — DeepSeek Flash is the clear default, and title generation keeps working as before. ### 2026-09-21 — Publisher API v1 is live *new · product · voidnet* Publisher API v1 is available today: manage API keys, read usage, and refresh tokens from code or the new Console tokens page. Domain verification now shows exactly what to prove, step by step. ### 2026-09-20 — Composer rebuilt: the clunk is gone *new · product · voidai* The composer used to lag and stutter — typing, drafts, attachments, and edits are instant now, tooltips behave, and tapping an attachment opens it in the workspace. ### 2026-09-20 — Tell us what's wrong, right where it happens *new · product · voidai, voidnet* Every header now carries a feedback button — in chat, on the site, in Console, across docs, even on shared links. Tap it, describe the problem in your own words, send. No account needed, and the page you're on travels with the report so the team sees exactly what you saw. ### 2026-09-20 — Real Word documents with charts, plus bigger reads *new · product · voidai* Ask for a document and get a real paged .docx with charts and a live preview — and VoidAI now reads files up to 15000 lines with longer answers. ### 2026-09-16 — A calmer shell, sources you can trust, tidier images *new · product · voidai* The sidebar and drawers glide instead of jumping, the header gets out of your way while you scroll, search answers gather their citations into one Sources panel, and grouped images open in a cleaner viewer. ### 2026-09-16 — Newsroom shows edit dates, new VoidAI guides *new · product · voidnet* Articles now show when they were last edited, alongside a rewritten VoidAI guide and a new workspaces explainer with screenshots. ### 2026-09-15 — Shared mindmaps render cleaner with follow-up prompts *new · product · voidai, voidnet* Opening a shared mindmap link shows a polished view with a pill to ask VoidAI a follow-up. ### 2026-09-15 — Seasonal artwork in the header *new · product · voidai* The header celebrates the season with light and dark artwork for Diwali, Halloween, Day of the Dead, Oktoberfest, and Mid-Autumn Festival. ### 2026-09-14 — Attach files to chat *new · product · voidai* Send files alongside your message, see them as chips, keep them in drafts, and let the assistant read their text — including PDFs. ### 2026-09-14 — Images group neatly, capable models see them *new · product · voidai* Multiple images in an answer arrange into clean grouped layouts with a better viewer, and models that support vision receive them while text-only models keep a safe default. ### 2026-09-14 — Tap a file name to open it *new · product · voidai* When the assistant mentions a workspace file, it appears as a tinted button that opens the file in its workspace view. ### 2026-09-14 — Slides with outline, stage, and present mode *new · product · voidai, voidnet* Build slide decks as a single file with an outline filmstrip, a fixed stage, and a chromeless present mode — shared links present the same way. ### 2026-09-13 — Same viewers everywhere, faster share links, self-healing sync *new · product · voidai, voidnet* Notes, mindmaps, PDFs, and pages look the same in the workspace and in shared links, share links open faster with proper error pages, and file sync recovers on its own when something is out of date. ### 2026-09-13 — Codespaces: a dev space with shell and editor *new · product · voidai* Open a code space with a terminal shell, file-aware commands, project switching, and syntax-highlighted editing in familiar themes. ### 2026-09-11 — Workspace files: code, PDF, quotes, safer sync *new · product · voidai* Code opens with highlighting, PDFs get a real viewer, selections can quote back into chat, and saving and syncing hold up under edits from multiple places. ### 2026-09-10 — VoidAI feels instant and installs like an app *new · product · voidai* Replies drip in live, the app boots faster with friendly empty states, greetings feel personal, and you can install it with offline support and short chat links. ### 2026-09-08 — API reference generated from the live spec *new · docs · voidnet* The API reference is now generated per product area with corrected fees, streaming, hosts, and model listing — grouped the way the docs navigate. ### 2026-09-07 — Knowledge cards, 13 languages, and a calmer VoidAI *new · product · voidai* Ask about a person, place, or thing and get a glanceable card with the key facts, all tappable for follow-ups. VoidAI now replies fully in 13 top world languages with regional dialects, renders wide tables that fit your screen, draws diagrams, and searches the web only when freshness actually matters. ### 2026-09-06 — Wallet, weekly payouts, and exact money everywhere *change · billing · voidnet* Top up a wallet and pay per use, get paid out every week as a publisher, and see exact amounts down to fractions of a cent across wallet, earnings, and analytics. New paid tiers are 80/20. ### 2026-09-04 — Default platform fee 20% *change · product · voidnet* New paid tiers default to publishers keeping 80%. Existing apps keep their locked rate; founding publishers keep 85/15. ### 2026-09-02 — AI models now publishable like MCP apps *new · product · voidnet* AI models can now be published inside Voidnet just like MCP apps. Find it in openvoidnet.com/console. ### 2026-09-01 — Share any answer as an image *new · product · voidai* Turn any AI reply into a polished shareable image card — preview it, download it, copy it, or send it straight to your apps. ### 2026-08-31 — Streamlined publishing with a single working place *new · product · voidnet* Publish in one clear flow — create a draft, verify your server, and set pricing in the same place. Drafts stay private until you publish. ### 2026-08-31 — Subscribed Apps in Console *new · product · voidnet* Find all your subscribed apps in one place and jump straight to usage. ### 2026-08-28 — Instant answers in VoidAI *new · product · voidai* Get beautiful inline cards for words, translations, definitions, grammar, and math — right where you ask. ### 2026-08-28 — Branch and revisit conversations *new · product · voidai* Edit any message, flip between versions, or branch a discussion into a fresh chat. ### 2026-08-25 — A cleaner Store and flexible model choice *new · product · voidai* Discover apps in a refined Store and use your own keys for top providers. ### 2026-08-25 — Documents that feel like real pages *new · product · voidai* Create paginated documents with live updates that keep your place. ### 2026-08-25 — Smoother, faster chat *fix · platform · voidai* Everyday chats feel quicker and handle busy moments more elegantly. ### 2026-08-24 — More reliable files and sharing *fix · product · voidai* Your files and shared links are now more predictable and polished. ### 2026-08-23 — A workspace built for making *new · product · voidai* Five dedicated views for your projects, documents, notes, mindmaps, and files. ### 2026-08-21 — Stateless MCP 2026-07-28 — sessions, initialize, and Mcp-Session-Id removed *breaking · gateway · voidnet* Every MCP request is now independent. No setup handshake, no sticky server — horizontally scalable and faster. ### 2026-08-21 — MCP-Protocol-Version + Mcp-Method/Name required, HeaderMismatch handling *breaking · gateway · voidnet* ### 2026-08-21 — server/discover + stateless list cache *new · gateway · voidnet* Clients can discover what a Void App supports before calling tools — and list results are now cached correctly per buyer. ### 2026-08-21 — Per-app revenue split — default 80/20 (was 70/30) *breaking · billing · voidnet* Publishers keep 80% by default. Negotiated deals can keep more on a single app without touching code. ### 2026-08-20 — Interactive OAuth — Authorization Code + PKCE + Refresh Tokens *new · auth · voidnet* Claude Desktop, Cursor, and VS Code can now log in via the gateway — open a browser, approve, and get a short-lived token without pasting keys. ### 2026-08-20 — VoidAI deep-link prompts (?q=) with auto-send *new · product · voidai, voidnet* Open VoidAI with a prompt in the URL and it sends automatically. ### 2026-08-19 — VoidAI multi-provider model selector + search images *new · product · voidai* Pick Groq, DeepSeek, or OpenAI — and see images with every search. ### 2026-08-10 — v1-beta gateway is live *new · gateway · voidnet* Voidnet is now routing requests. Publish and consume MCP tools today. ### 2026-08-10 — OAuth 2.0 authorization server *new · auth · voidnet* ### 2026-08-10 — VoidAI keys supported *new · auth · voidnet, voidai* VoidAI now works seamlessly with Voidnet. ### 2026-08-10 — Scope enforcement *new · auth · voidnet* ### 2026-08-10 — Correct MCP session forwarding (historical — removed 2026-08-21) *fix · gateway · voidnet* ### 2026-08-10 — Security hardening *new · security · voidnet* Voidnet is now harder to abuse. ### 2026-08-10 — Stripe billing *new · billing · voidnet* Publishers can now charge for their apps. # Domain Verification (/docs/domain-verification) Publishing requires a verified domain. Verification is one file, served over HTTPS, checked from the Voidnet side. Console setup lives under Console → Domains. ## The canonical address [#the-canonical-address] Serve the verification file with HTTP 200 at: ``` https://YOUR-DOMAIN/.well-known/voidnet-site-verification-.html ``` The root path (`https://YOUR-DOMAIN/voidnet-site-verification-.html`) stays accepted as legacy, but the canonical address above is checked first. Serving both is harmless; serving only the canonical address is sufficient. ## What gets checked [#what-gets-checked] * HTTPS with a valid certificate, 15-second timeout, 64 KB body cap. * Exact body match against the issued file content. * Redirects are **not** followed — a 301/308 to another host or path fails the check. Serve the file directly at the checked address. * No authentication may stand in front of the file. CDN challenge pages and login walls fail closed. ## Reading a failure [#reading-a-failure] A failed verification returns per-address evidence — the same table the Console renders inline. Each row names the URL tried, the HTTP status received, any redirect target, and the fetch error: | Column | Meaning | | --------- | ---------------------------------------------------------------------------------- | | URL tried | The exact address fetched, in check order | | Status | HTTP status received (`—` means no response arrived) | | Redirect | `location` header when one was sent (informational — redirects are never followed) | | Error | Transport cause: DNS failure (`ENOTFOUND`), timeout, TLS rejection, oversize body | Match these rows against your own server and CDN logs: a missing row on your side with `ENOTFOUND` on ours means DNS never resolved; a 301 row with a `location` means the file lives behind a redirect you must remove. ## Common causes, in order [#common-causes-in-order] 1. **DNS** — the domain does not resolve publicly (`ENOTFOUND`). Propagated locally but not globally is the classic variant. 2. **Redirects** — apex-to-www, HTTP-to-HTTPS-chain, or trailing-slash rewrites in front of the file. Flatten them for this path. 3. **CDN challenge or bot wall** — Cloudflare-style interstitials return HTML, not the file. Exempt the verification path. 4. **Auth or IP allowlists** — the fetch comes from Voidnet infrastructure, not your browser session. Anything your browser passes and a stranger fails will fail here too. ## Ownership [#ownership] One domain belongs to one account. A domain already verified on any account cannot be verified again elsewhere: re-initiating it on the same account reports it as already verified, and initiating it on a different account is rejected while the existing verification stands. ## Expiry [#expiry] A verification record expires 7 days after initiation. Verify inside that window; expired records need a fresh initiation, which issues a new token. # Ecosystem (/docs/ecosystem) Voidnet Console is a platform for building, publishing, and consuming AI-powered apps. Here is every piece and what it does. ## Platform services [#platform-services] ### Voidnet Console [#voidnet-console] The web interface for managing your account, apps, API keys, billing, and marketplace presence. [Jump to Console](https://openvoidnet.com/console) ### VoidAI [#voidai] openvoidnet's AI assistant — discover and interact with apps through natural language. [Jump to VoidAI](https://chat.openvoidnet.com) ### Voidnet [#voidnet] The routing engine that proxies requests from buyers to publisher servers. Handles authentication, subscriptions, rate limiting, metering, and billing on every request. [Read the Voidnet docs](https://docs.openvoidnet.com/docs/gateway) ## Account management [#account-management] ### Buyer accounts [#buyer-accounts] Create an account, generate API keys, purchase apps, and track usage. [Get Started](https://docs.openvoidnet.com/docs/get-started) ### Publisher accounts [#publisher-accounts] Register as a publisher, verify your domain, build apps, and connect Stripe for payouts. [Publisher Guide](https://docs.openvoidnet.com/docs/publisher-guide/mcp) ## Billing [#billing] ### Stripe Connect [#stripe-connect] Publishers receive payouts to their connected Stripe accounts. The per-app platform fee (default 20%) is charged on paid transactions; the publisher keeps 80% (configurable per app — negotiated rates can keep more). ### Metering [#metering] Usage is tracked per buyer per app. Free and paid tiers have publisher-defined monthly caps on a rolling 30-day window from the purchase date. ## Developer resources [#developer-resources] ### Documentation [#documentation] You're here. Guides, references, and examples for building on Voidnet Console. ### GitHub [#github] SDKs, reference implementations, and examples. [Jump to GitHub](https://github.com/openvoidnet/) ### npm [#npm] JavaScript and TypeScript SDKs for building and consuming Void Apps. [Jump to npm](https://www.npmjs.com/package/openvoidnet) ### PyPI [#pypi] Python SDKs for building and consuming Void Apps. [Jump to PyPI](https://pypi.org/project/openvoidnet/) # Error Reference (/docs/error-reference) Every gateway error uses one JSON shape. The `error` field is an **object** with `code`, `message`, and `status` — not a string. ```json { "error": { "code": "api_key_invalid", "message": "Invalid token format. Expected: vnb-sk-xxx or JWT", "status": 401 } } ``` OAuth endpoints (`POST /oauth/token`, `GET/POST /authorize`, `/.well-known/*`) use a **different** shape: `{"error": "", "error_description": ""}`. These are noted below. *** ## Authentication — `401` [#authentication--401] Returned when the `Authorization` header is missing, malformed, or the credential is unknown/expired. | Code | Condition | Fix | | ----------------- | ------------------------------------------------------------------- | --------------------------------------------------- | | `api_key_missing` | No `Authorization` header | Add `Authorization: Bearer ` | | `api_key_invalid` | Key format is wrong, JWT header/signature invalid, or key not found | Key prefixes: `vnb-sk-` or a 3-segment JWT | | `api_key_expired` | JWT access token expired | Request a new token from `POST /oauth/token` | | `api_key_revoked` | Key was revoked in the Console | Generate a new key | | `buyer_not_found` | JWT is valid but the buyer no longer exists | Request a new token; contact support if it persists | The gateway auto-detects the credential type: a 3-segment (dot-separated) value is treated as a JWT; a `vnb-sk-` prefix is treated as an API key. WWW-Authenticate header is set on `401` (missing/invalid/expired key) and `403 insufficient_scope` responses so OAuth-aware clients can bootstrap discovery. ## OAuth — `400` / `401` / `405` / `500` [#oauth--400--401--405--500] Returned by `POST /oauth/token`, `GET/POST /authorize`, and the `/.well-known/*` metadata endpoints. **These use the OAuth error shape, not the gateway envelope.** | Status | Code | Condition | | ------ | --------------------------- | ----------------------------------------------------------------------------------------------- | | 400 | `invalid_request` | Body unreadable or Content-Type is not `application/x-www-form-urlencoded` / `application/json` | | 400 | `unsupported_grant_type` | `grant_type` is not `client_credentials`, `authorization_code`, or `refresh_token` | | 400 | `invalid_client` | `client_id` empty, bad prefix, or `client_secret` mismatch | | 400 | `invalid_grant` | Authorization code invalid/expired, PKCE mismatch, refresh token reused/rotated | | 400 | `unsupported_response_type` | `response_type` is not `code` | | 400 | `invalid_request` | Missing required params (`client_id`, `redirect_uri`, `code_challenge`, etc.) | | 401 | `invalid_client` | Credential lookup failed (key not found) | | 405 | `unsupported_method` | Wrong HTTP method on a `/.well-known/*`, `/oauth/token`, or `/authorize` endpoint | | 500 | `server_error` | Token signing not configured or signing failed | | 404 | `not_found` | JWKS requested but signing not configured (on `/.well-known/jwks.json`) | ## Protocol — `400` [#protocol--400] Returned for MCP protocol version mismatches and header validation. | Status | Code | Condition | Fix | | ------ | --------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | 400 | `UnsupportedProtocolVersion` | `MCP-Protocol-Version` header not `2026-07-28` | Use `2026-07-28` (MCP only) | | 400 | `HeaderMismatch` | `MCP-Protocol-Version` header != `_meta.protocolVersion` in request body | Make both match `2026-07-28` (MCP only) | | 400 | `MissingRequiredClientCapability` | Client lacks a capability required by the method | Add capability in `_meta.clientCapabilities` (MCP only) | | 400 | `invalid_request` | Missing required headers (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`) | Add all three headers (MCP only) | | 400 | `invalid_request` | LLM `model` or `messages` missing, or invalid JSON | Include `{"model":"...","messages":[{"role":"user","content":"..."}]}` (LLM only) | | 400 | `invalid_request` | LLM multimodal `image_url/audio/video` present | TEXT only — send `content: string` only (LLM) | | 502 | `publisher_error` | LLM stream ended with no `usage.total_tokens` chunk | Publisher must send a final usage chunk (LLM streaming) | ## Billing — `402` / `403` / `429` [#billing--402--403--429] Returned after authentication, when the purchase/subscription check or wallet deduct fails. | Status | Code | Condition | Fix | | ------ | ------------------------ | ----------------------------------------- | ---------------------------------------------------------------- | | 402 | `insufficient_balance` | Wallet balance can't cover the call price | Top up at **Console → Wallet** ($5–$1000 via Stripe), then retry | | 403 | `app_not_purchased` | No purchase or subscription record | Purchase (or "Get" a free tier) in the Marketplace | | 403 | `subscription_inactive` | Subscription exists but isn't active | Check billing status in the Console | | 403 | `subscription_expired` | Subscription period ended | Renew in the Console | | 403 | `subscription_cancelled` | Subscription was cancelled | Contact support or repurchase | | 429 | `meter_expired` | Billing period expired, meter needs reset | Wait for meter reset | ## Metering — `400` / `501` [#metering--400--501] Returned when the gateway can't record usage for the call. Rare — the call itself reached the publisher. | Status | Code | Condition | Fix | | ------ | ------------------------ | ---------------------------------------- | -------------------------------------------- | | 400 | `invalid_record` | Metering record failed validation | Retry; contact support if it persists | | 400 | `invalid_timestamp` | Metering timestamp rejected | Retry; contact support if it persists | | 501 | `protocol_not_supported` | A2A metering attempted — A2A is not live | A2A is not available yet; use `mcp` or `llm` | ## Rate limiting — `429` [#rate-limiting--429] Returned when a usage limit is exceeded. All limits use fixed-window counters. | Status | Code | Condition | Fix | | ------ | ---------------------- | --------------------------------------------- | ----------------------------------------------------- | | 429 | `rate_limit_exceeded` | Request rate exceeded (per-minute or per-day) | Wait for the window to reset, then retry with backoff | | 429 | `meter_limit_exceeded` | Monthly request quota exhausted | Wait for the rolling 30-day reset, or upgrade tier | `429` responses do **not** include a `details` payload. There are no `X-RateLimit-*` response headers. Track your usage in the Console. ## Suspension / Scope — `403` [#suspension--scope--403] | Status | Code | Condition | | ------ | --------------------- | -------------------------------------- | | 403 | `buyer_suspended` | Buyer account suspended | | 403 | `publisher_suspended` | Publisher account suspended | | 403 | `insufficient_scope` | JWT scope doesn't permit the operation | ## Routing — `400` / `404` [#routing--400--404] | Status | Code | Condition | Fix | | ------ | ----------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | | 400 | `invalid_path` | URL doesn't match `/v1-beta/{adapter}/{username}/{appname}` | Check the path | | 400 | `invalid_adapter_type` | Defensive only — unknown adapters never reach validation (routing returns `404 not_found` first) | Adapter segment is `mcp` or `llm` | | 400 | `invalid_adapter` | Defensive only — route and adapter always match after routing | Use `mcp` for tools, `llm` for chat | | 400 | `invalid_request` | LLM missing `model`/`messages` or multimodal | `model` and `messages: [{role, content:string}]` required, TEXT only | | 400 | `invalid_request` | Missing fields or invalid JSON-RPC structure | Check the JSON-RPC body | | 404 | `app_not_found` | No app registered for that username/appname | Verify both names | | 404 | `publisher_not_found` | Publisher username doesn't exist | Check the username | | 404 | `publisher_api_key_not_found` | Publisher has no active API key | Publisher must regenerate their key | | 404 | `purchase_not_found` | Purchase record missing during usage query | Confirm the purchase | ## Publisher — `4xx` / `5xx` [#publisher--4xx--5xx] Returned when the gateway proxies to the publisher and the publisher's server responds with an error. | Status | Code | Condition | | | ------ | ------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | 4xx | `publisher_client_error` | Publisher returned a 400–499 (their error — check their docs) | | | 5xx | `publisher_server_error` | Publisher returned a 500+ (their server error) | | | 502 | `publisher_error` | Publisher transport/initialization failure | | | 502 | `app_offline` | App server unreachable — circuit opened after repeated failures | Retry shortly; contact the publisher if it persists | | 500 | `publisher_not_payable` | Paid call blocked — publisher has no connected payout account | Contact the publisher | | 504 | `gateway_timeout` | Publisher didn't respond in time | | | 501 | `adapter_not_supported` | Requested adapter isn't available (available: `mcp`, `llm`) | | | 502 | `publisher_error` (LLM) | Publisher returned invalid JSON or missing `usage.total_tokens` | Publisher must return `{"choices":[{"message":{"content":"..."} }],"usage":{"total_tokens":...}}` | ## Request — `400` / `404` / `405` [#request--400--404--405] | Status | Code | Condition | | | ------ | -------------------- | --------------------------------------------------------------------------------------- | ------------------------------------- | | 400 | `bad_request` | Unparseable request | | | 413 | `body_too_large` | Request body exceeds the size limit | | | 404 | `not_found` | No route matches the URL | | | 405 | `method_not_allowed` | PUT/DELETE on a known route (GET routes to the pipeline; POST is the only write method) | Use POST (PUT/DELETE are never valid) | ## Internal — `500` [#internal--500] | Status | Code | Condition | | ------ | -------------------- | ----------------------------------------------- | | 500 | `internal_error` | Server-side error — retry, then contact support | | 500 | `database_error` | Database query/write failed | | 500 | `redis_error` | Rate-limit cache operation failed | | 500 | `trace_error` | Failed to start request tracing (non-critical) | | 500 | `invalid_app_config` | App configuration is invalid | # Voidnet (/docs/gateway) The Voidnet is the central routing layer of the Voidnet Console platform. You send it a request; it authenticates you, enforces your subscription and usage limits, and proxies the request to the publisher's server. You never connect to a publisher directly. ``` Buyer (API key or JWT) → Voidnet → Publisher's server ``` ## What the gateway does [#what-the-gateway-does] 1. **Authenticates** your credential (API key or JWT). 2. **Checks your purchase** of the app and your tier's meter. 3. **Enforces rate limits** (per-minute, per-day, monthly meter). 4. **Routes** the request to the publisher's server. 5. **Returns** the publisher's response, or a gateway error if something fails. You do not need to know the publisher's server URL. The gateway resolves it from the `username/appname` in your request path. ## Endpoint [#endpoint] All app requests go through one endpoint. The adapter in the path selects the protocol: ``` POST /v1-beta/{adapter}/{username}/{appname} ``` | Parameter | Value | | ---------------------- | --------------------------------------------------------------------------------- | | `adapter` | `mcp` or `llm` — see [Void Apps](https://docs.openvoidnet.com/docs/void-apps) | | `username` | Publisher's public username | | `appname` | App name, unique per publisher | | `Authorization` | `Bearer `, or `Bearer ` (scope `mcp:tools` or `llm:completions`) | | `MCP-Protocol-Version` | `2026-07-28` (required for `mcp` only) | | `Mcp-Method` | JSON-RPC method name (required for `mcp` only) | | `Mcp-Name` | Tool/resource/prompt name (required for `mcp` only) | **MCP — Stateless 2026-07-28:** * Only `POST` is supported. `GET` and `DELETE` return `405 Method Not Allowed`. * SSE streams are per-request on POST; close = cancel. No `Last-Event-ID`, no `Mcp-Session-Id`. **LLM — OpenAI compatible:** ``` POST /v1-beta/llm/{username}/{appname} Authorization: Bearer vnb-sk-... Content-Type: application/json Body: {"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"hi"}],"stream":false} ``` * One model per app — the `model` you send must match the app's published model exactly. * `stream:true` is supported (SSE); the gateway injects `stream_options.include_usage:true` when missing, and streams without a final usage chunk fail loudly instead of going unbilled. Multimodal inputs (`image_url`, `audio`) are rejected — TEXT only. * Your LLM server must expose `POST {serverUrl}/chat/completions` (`{serverUrl}` is your configured Server URL, which already ends in `/v1`) and accept any `Authorization: Bearer ` header. Plus operational endpoints: | Method | Path | Purpose | | ------ | ----------------------------------------- | ---------------------------------------------------------------------------------- | | `GET` | `/health` | Health check (db + redis) | | `POST` | `/oauth/token` | Issue JWT access tokens (client\_credentials, authorization\_code, refresh\_token) | | `GET` | `/.well-known/oauth-authorization-server` | OAuth metadata (RFC 8414) | | `GET` | `/.well-known/oauth-protected-resource` | OAuth metadata (RFC 9728) | | `GET` | `/.well-known/jwks.json` | Public signing keys | | `GET` | `/authorize` | OAuth 2.1 authorization code + PKCE, login and consent pages (interactive clients) | | `POST` | `/authorize` | Submit login + consent, issue authorization code (interactive clients) | For the full request/response schema of every operation, see the [API Reference](https://docs.openvoidnet.com/docs/reference/api-reference) — it is generated from the gateway source and cannot drift. ## Authentication [#authentication] Two credential types, both in the `Authorization: Bearer` header. The gateway auto-detects which you're using: * **API keys** (`vnb-sk-*`) — long-lived, generated in the Console. * **JWT access tokens** — short-lived (1 hour), obtained from `POST /oauth/token`. Use these in production. A 3-segment (dot-separated) value is treated as a JWT; a `vnb-sk-` prefix is treated as an API key. ## Errors [#errors] All gateway errors use one shape (OAuth endpoints are the exception — see [Error Reference](https://docs.openvoidnet.com/docs/error-reference#error-reference)): ```json { "error": { "code": "error_code", "message": "Human-readable description", "status": 429 } } ``` # Get Started (/docs/get-started) Get credentials and call a published Void App through the Voidnet. Every request uses the same pattern regardless of the app. ## Prerequisites [#prerequisites] * A Voidnet Console account ([sign up](https://openvoidnet.com/console)) * An HTTP client ([curl](https://curl.se), Postman, or any language) ## Step 1: Get credentials [#step-1-get-credentials] You need a bearer credential to authenticate requests. Two options: ### Option A: API key (simplest) [#option-a-api-key-simplest] In the Console, open **API Keys** → **Generate Key**. You get a key like: ``` vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6 ``` **Save it now** — it is shown only once. Lost keys must be revoked and regenerated. | Prefix | Type | Use case | | --------- | ----------------- | --------------- | | `vnb-sk-` | Buyer service key | General purpose | ### Option B: OAuth access token (production) [#option-b-oauth-access-token-production] Exchange an API key for a short-lived JWT. Better for production because tokens expire and can be scoped. ```bash curl -X POST https://api.openvoidnet.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" ``` Response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "scope": "mcp:tools" } ``` The `access_token` is a JWT valid for 1 hour. Use it as a `Bearer` credential anywhere you would use an API key. When it expires the gateway returns `401 api_key_expired` — request a new token and retry. The `scope` defaults to `mcp:tools`. A token with an `mcp:` prefix can call any MCP method; `llm:completions` can call LLM `chat/completions`. Example `llm:completions` or `mcp:tools llm:completions`. Empty scope (legacy keys) is unrestricted. ## Step 2: Find an app [#step-2-find-an-app] Browse the [Marketplace](https://openvoidnet.com/marketplace). Note two things about the app you want: * **Publisher username** — the developer's public identifier (e.g. `acmecorp`) * **App name** — unique per publisher (e.g. `weather-tools`) Purchase the app to gain access. Apps offer free tiers, paid tiers, or both: * **Free tier** — click Get Free. Access is instant. * **Paid tier** — click Subscribe/Purchase. Paid access runs on wallet: top up at **Console → Wallet** ($5–$1000 via Stripe), and the price deducts automatically. * **Upgrade** — own free and want paid? The app page shows an Upgrade button; free access is revoked when the paid grant completes. **Stateless MCP 2026-07-28:** You do not need sessions, initialize handshake, or SSE to call a tool. Just POST with the required headers. ## Step 3: Send your first request [#step-3-send-your-first-request] The URL encodes the protocol, publisher, and app: ``` POST /v1-beta/{adapter}/{username}/{appname} ``` For MCP tools the adapter is `mcp`. For AI models the adapter is `llm`. Example using the test app: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-a1b2c3d4e5f6..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/list" }' ``` **Required headers:** * `Authorization: Bearer ` * `MCP-Protocol-Version: 2026-07-28` * `Mcp-Method: ` (e.g., `tools/list`) * `Mcp-Name: ` (e.g., `tools/list`) **Stateless MCP 2026-07-28** — No sessions, no initialize handshake, no `Mcp-Session-Id`, no GET/DELETE endpoints. Response (the exact tools depend on the publisher): ```json { "jsonrpc": "2.0", "id": "1", "result": { "tools": [ { "name": "get_forecast", "description": "Get weather forecast for a location", "inputSchema": { "type": "object", "properties": { "location": { "type": "string", "description": "City name or coordinates" }, "days": { "type": "integer", "description": "Number of days (1-7)" } }, "required": ["location"] } } ], "ttlMs": 300000, "cacheScope": "private" } } ``` ## Step 4: Call a tool [#step-4-call-a-tool] Use `tools/call` with the tool name and arguments: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-a1b2c3d4e5f6..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_forecast" \ -d '{ "jsonrpc": "2.0", "id": "2", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } }' ``` ### Try LLM [#try-llm] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm \ -H "Authorization: Bearer vnb-sk-a1b2c3..." \ -H "Content-Type: application/json" \ -d '{"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"Hello!"}],"stream":false}' # Or OpenAI SDK: # from openai import OpenAI; client = OpenAI(base_url="https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm", api_key="vnb-sk-...") # client.chat.completions.create(model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role":"user","content":"hi"}]) ``` ## Next steps [#next-steps] * [Buyer Guide (MCP)](https://docs.openvoidnet.com/docs/mcp/buyer) — OAuth (client\_credentials, authorization\_code+PKCE, refresh\_token), rate limits, advanced patterns * [Buyer Guide (LLM)](https://docs.openvoidnet.com/docs/llm/buyer) — OpenAI-compatible calls via SDK * [Publisher Guide (LLM)](https://docs.openvoidnet.com/docs/llm/publisher) — host with vLLM/Ollama and publish * [API Reference](https://docs.openvoidnet.com/docs/reference/api-reference) — complete, generated from the gateway source * [Error Reference](https://docs.openvoidnet.com/docs/reference/error-reference) — full error code catalog # Docs (/docs) Welcome to the Voidnet Console developer documentation. **Quickstart:** New here? [Get Started](https://docs.openvoidnet.com/docs/get-started) — credentials and your first call. ## AI agent instructions [#ai-agent-instructions] Working with an AI coding agent? Copy this prompt and paste it into your AI — it teaches your AI to work with openvoidnet programmatically. ```text I need to work with openvoidnet programmatically. Start here: https://docs.openvoidnet.com/llms.txt — it is the complete docs navigation. Fetch only the pages you need, and treat the docs as ground truth for protocol versions, auth, endpoints, errors, and billing. Never guess an endpoint or a rule. What lives where: - https://docs.openvoidnet.com — full developer documentation (get started, auth, publish, reference) - https://openvoidnet.com/marketplace — browse Void Apps: tools, agents, models - https://openvoidnet.com/newsroom — blogs and news; see what is possible with openvoidnet - https://chat.openvoidnet.com — use Void Apps through chat, like a consumer - https://accounts.openvoidnet.com — account management (sign in, API keys live here) Rules: 1. Map first via llms.txt, then read the exact page before acting. 2. One URL, one key: every call carries the caller's credential; never invent keys or tokens. 3. Confirm protocol versions from the docs before building — do not assume. 4. If the docs and your memory disagree, the docs win. ``` ## I want to... [#i-want-to] * [Call a tool](https://docs.openvoidnet.com/docs/get-started) — Get credentials and make your first request * [Use OAuth, sessions, or SSE](https://docs.openvoidnet.com/docs/mcp/buyer) — Advanced buyer patterns * [Publish my own server](https://docs.openvoidnet.com/docs/mcp/publisher) — Build and list an MCP app * [Understand auth tokens](https://docs.openvoidnet.com/docs/mcp/oauth) — OAuth 2.0 access tokens * [Debug an error](https://docs.openvoidnet.com/docs/reference/error-reference) — Complete error code catalog * [Check my rate limits](https://docs.openvoidnet.com/docs/reference/rate-limiting) — Rate limiting and metering ## Platform [#platform] * [Ecosystem](https://docs.openvoidnet.com/docs/learn/ecosystem) — Console, VoidAI, Voidnet, and developer resources * [Void Apps](https://docs.openvoidnet.com/docs/learn/void-apps) — App types: MCP tools, A2A agents, LLM, SLM * [API Reference](https://docs.openvoidnet.com/docs/reference/api-reference) — Generated from the gateway source * [Changelog](https://docs.openvoidnet.com/docs/changelog) — Latest platform updates # Void Apps (/docs/void-apps) Void Apps are services published by developers and made available to buyers through the marketplace. The Voidnet routes buyer requests to the publisher's server, handling authentication, rate limiting, metering, and billing. Your code only talks to the gateway. ## App types [#app-types] ### MCP — AI Tools (available) [#mcp--ai-tools-available] [Model Context Protocol](https://modelcontextprotocol.io) servers expose callable tools, data resources, and prompt templates. AI clients discover and invoke these tools at runtime. Protocol version `2026-07-28` (stateless). An MCP server can expose: * **Tools** — callable functions with typed JSON input schemas. * **Resources** — URI-addressable data. * **Prompts** — reusable prompt templates. Available today: MCP and AI models. A2A 1.0 is `1.0` not yet, SLM not yet. ### A2A — AI Agents (not available) [#a2a--ai-agents-not-available] [Agent-to-Agent Protocol](https://github.com/google/A2A) enables communication between autonomous AI agents. Protocol version `1.0`. Not yet supported. ### LLM — Language Model Integration (available) [#llm--language-model-integration-available] Connect and query language model providers through a unified OpenAI-compatible interface. **Text output only** in V1 — no image, audio, or video. One model per app, token-metered (`usage.total_tokens`), billed per token. Host with vLLM/Ollama at `https://your-host` and expose `POST https://your-host/v1/chat/completions`. Example: `HuggingFaceTB/SmolLM2-135M-Instruct` on `http://localhost:6010/v1` or Ollama `http://localhost:11434/v1`. ### SLM — Specialized Language Models (not available) [#slm--specialized-language-models-not-available] Smaller, domain-tuned language models. Not yet supported. ## How apps are consumed [#how-apps-are-consumed] ### MCP (stateless 2026-07-28) [#mcp-stateless-2026-07-28] ``` POST /v1-beta/mcp/{username}/{appname} Authorization: Bearer vnb-sk-... # API key Authorization: Bearer eyJhbGciOiJSUzI1... # JWT access token Headers: MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: get_forecast ``` ### LLM (OpenAI compatible) [#llm-openai-compatible] ``` POST /v1-beta/llm/{username}/{appname} # gateway Authorization: Bearer vnb-sk-... # buyer key Content-Type: application/json Body: {"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"hi"}],"temperature":0.7,"max_tokens":64,"stream":false} # With OpenAI SDK: # from openai import OpenAI; client = OpenAI(base_url="https://api.openvoidnet.com/v1-beta/llm/{publisher}/{app}", api_key="vnb-sk-...") # client.chat.completions.create(model="...", messages=[{"role":"user","content":"hi"}]) ``` 1. Browse the Marketplace and purchase an app (free tiers require clicking "Get"). 2. Note the publisher's **username** and the app's **name**. 3. Send JSON-RPC 2.0 requests through the gateway. **Stateless MCP 2026-07-28** — No sessions, no initialize handshake, no `Mcp-Session-Id` header, no GET/DELETE endpoints. SSE streams are per-request on POST; close = cancel. The gateway accepts both API keys and JWT access tokens and auto-detects the credential type. ## How apps are published [#how-apps-are-published] Publishers register their app, choose its type (`mcp` or `llm`), and provide their server URL (`https://host/v1` for LLM, `https://host/mcp` for MCP). The gateway stores the mapping between `username/appname` and the server. When a buyer makes a request, the gateway resolves the server URL, authenticates, validates the purchase, and proxies the request. LLM apps are one model per app — the published model ID must match the `model` buyers send. Publishers who want to build and list an app should read the [Publisher Guide (MCP)](https://docs.openvoidnet.com/docs/publisher-guide/mcp) or [Publisher Guide (LLM)](https://docs.openvoidnet.com/docs/publisher-guide/llm). # A2A Buyer Guide (/docs/buyer-guide/a2a) > **Coming soon** — A2A (Agent-to-Agent) protocol support is in development. This guide will cover how to discover, purchase, and interact with AI agents published on Voidnet Console using the A2A protocol. ## What to expect [#what-to-expect] ### Overview [#overview] * What A2A agents are and how they differ from MCP tools * The Agent-to-Agent protocol and how agents discover and communicate with each other ### Discovery [#discovery] * Browsing A2A agents in the marketplace * Understanding agent capabilities and task types ### Authentication [#authentication] * API keys and OAuth tokens for agent communication * How the Voidnet authenticates agent-to-agent requests ### Making requests [#making-requests] * Sending tasks to agents * Receiving streaming responses * Agent card and skill discovery ### Protocol reference [#protocol-reference] * A2A protocol version `1.0` methods and message formats * Task lifecycle: submitted → working → completed → failed * Streaming vs polling modes *** Check back closer to launch for the full guide. For now, see the [MCP Buyer Guide](https://docs.openvoidnet.com/docs/buyer-guide/mcp) to start using tools on Voidnet Console today. # LLM Buyer Guide (/docs/buyer-guide/llm) Call a publisher's AI model through Voidnet. One model per app, billed per token. Use the OpenAI SDK — just change the `base_url`. ## Credentials [#credentials] Use an API key from **Console → API Keys** (`vnb-sk-*`) or a short-lived JWT from `POST /oauth/token` (`grant_type=client_credentials`, `scope=llm:completions`). The gateway accepts both. ## Find an app [#find-an-app] Browse the Marketplace and note the publisher's username, the app name, and the model ID listed on the app page (for example `Qwen/Qwen2.5-7B-Instruct`). Purchase the app to gain access. ## Call it [#call-it] ### cURL [#curl] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm \ -H "Authorization: Bearer vnb-sk-a1b2c3..." \ -H "Content-Type: application/json" \ -d '{"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"Write a haiku"}],"temperature":0.7,"max_tokens":64,"stream":false}' ``` Response (OpenAI-compatible): ```json { "id": "chatcmpl-abc", "object": "chat.completion", "created": 1730000000, "model": "Qwen/Qwen2.5-7B-Instruct", "choices": [{"index":0,"message":{"role":"assistant","content":"An old pond..."},"finish_reason":"stop"}], "usage": {"prompt_tokens":10,"completion_tokens":12,"total_tokens":22} } ``` ### OpenAI SDK [#openai-sdk] ```python from openai import OpenAI client = OpenAI(base_url="https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm", api_key="vnb-sk-a1b2c3...") response = client.chat.completions.create(model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role":"user","content":"hi"}]) print(response.choices[0].message.content, response.usage.total_tokens) ``` ```ts import OpenAI from "openai" const openai = new OpenAI({ baseURL: "https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm", apiKey: "vnb-sk-..." }) const response = await openai.chat.completions.create({ model: "Qwen/Qwen2.5-7B-Instruct", messages: [{role:"user",content:"hi"}] }) ``` The only changes from a standard OpenAI call are the `baseURL` (your app's gateway URL) and the `model` (must match the app's published model). ## Limits V1 [#limits-v1] * TEXT only — `content` must be a string. `image_url`, `audio`, and `video` are not supported. * `stream:true` supported (SSE, OpenAI shape) — the gateway injects `stream_options.include_usage:true` when missing; streams without a final usage chunk fail loudly instead of going unbilled. * `chat/completions` for inference plus the read-only models listing (`GET` any path containing `models`) — no embeddings. # MCP Buyer Guide (/docs/buyer-guide/mcp) This guide covers everything you need to discover, purchase, and query apps published by developers on the Voidnet Console marketplace. New to Voidnet? [Get Started](https://docs.openvoidnet.com/docs/get-started) walks you through credentials and your first call. *** ## Overview [#overview] A **buyer** is a user who discovers apps in the Voidnet Console marketplace and calls them through the Voidnet. The Voidnet handles authentication, rate limiting, metering, and billing — so you only need valid credentials and the app's public name. ``` Your Client (Key or OAuth Token) → Voidnet → Publisher's MCP Server ``` The Voidnet supports three authentication methods: | Method | Credential | Use Case | | ----------------------------------- | ----------------------- | ----------------------------------------------------- | | **API Keys** | `vnb-sk-*` | Simple, long-lived secrets for developers | | **OAuth Client Credentials** | JWT from `/oauth/token` | Production systems, CI/CD | | **OAuth Authorization Code + PKCE** | JWT from `/oauth/token` | Interactive clients (Claude Desktop, Cursor, VS Code) | *** ## Prerequisites [#prerequisites] * A Voidnet Console account ([sign up](https://openvoidnet.com)) * An API key for authentication (see below) * Basic understanding of JSON-RPC 2.0 and MCP protocol *** ## Authentication [#authentication] ### API Keys [#api-keys] API keys are the simplest way to authenticate. Generate them from the **API Keys** page under the **Buyer** section of your [Console dashboard](https://openvoidnet.com/console): | Prefix | Type | Use Case | | --------- | ----------------- | --------------- | | `vnb-sk-` | Buyer Service Key | General purpose | ``` vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6 ``` **Security rules:** * Save your key immediately — it's shown only once at creation * Never expose keys in client-side code, version control, or logs * Rotate keys periodically from the Console * Revoke compromised keys immediately ### OAuth Access Tokens [#oauth-access-tokens] For production systems, you can use OAuth 2.0 access tokens instead of raw API keys. The Voidnet runs its own authorization server and supports three grant types: | Grant Type | Use Case | | -------------------- | --------------------------------------------------------------- | | `client_credentials` | Machine-to-machine (API key → short-lived JWT) | | `authorization_code` | Interactive clients (Claude Desktop, Cursor, VS Code) with PKCE | | `refresh_token` | Rotate expired access tokens without re-login | Access tokens are JSON Web Tokens (JWTs) signed with RS256. They expire after a configurable period (default 1 hour) and can be used anywhere you'd use an API key. ### When to use which [#when-to-use-which] | Situation | Recommended | | ------------------------------------------------ | -------------------------------------------- | | Local development, testing | API key (`vnb-sk-*`) | | Production services, CI/CD | JWT access token (client\_credentials) | | Interactive AI clients (Claude, Cursor, VS Code) | Authorization Code + PKCE | | Highly sensitive environments | Rotate API key → short-lived JWT per session | *** ## Getting an Access Token [#getting-an-access-token] ### Client Credentials (machine-to-machine) [#client-credentials-machine-to-machine] Exchange your `vnb-sk-*` key for a JWT access token: ```bash curl -X POST https://api.openvoidnet.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" ``` Successful response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600, "scope": "mcp:tools" } ``` The returned `access_token` is a JWT you can use in the `Authorization` header instead of your raw API key. **Notes:** * The `client_id` is your full API key — no separate `client_secret` is needed * A `scope` parameter may be provided (optional, defaults to `mcp:tools`) * The `aud` claim is always the Voidnet issuer URL — the token endpoint accepts no client-controlled audience input ### Authorization Code + PKCE (interactive clients) [#authorization-code--pkce-interactive-clients] For interactive clients (Claude Desktop, Cursor, VS Code) with a human in the loop: 1. Client discovers metadata from `GET /.well-known/oauth-authorization-server` 2. Client opens browser to `GET /authorize?client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256&response_type=code` 3. User logs in and consents on the gateway-hosted page 4. Gateway redirects to `redirect_uri?code=...&state=...` 5. Client exchanges code for tokens: `POST /oauth/token` with `grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier` 6. Response includes `access_token` (JWT), `refresh_token` (30-day, rotated on use) **Required PKCE parameters:** * `code_challenge` — 43-char base64url (S256 of `code_verifier`) * `code_challenge_method` — must be `S256` * `code_verifier` — 43-128 char unreserved string (used at token exchange) ### Refresh Token (rotate without re-login) [#refresh-token-rotate-without-re-login] ```bash curl -X POST https://api.openvoidnet.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=" \ -d "client_id=vnb-sk-..." ``` Response includes new `access_token` + **rotated** `refresh_token`. Old refresh token is invalidated. ### Token expiration [#token-expiration] Access tokens have an `exp` claim. When a token expires, the Voidnet returns `api_key_expired` (401). You should: 1. Detect the `401` response 2. Request a new token from `/oauth/token` (client\_credentials or refresh\_token) 3. Retry the request with the new token *** ## Apps and Purchasing [#apps-and-purchasing] Browse the [Marketplace](https://openvoidnet.com/marketplace). Each app lists its publisher, name, description, tier (free/paid), and capabilities. To query an app you need the **publisher's username** and **app name** — these form the public route. * **Free tier:** click "Get" to start using immediately (rate limits apply). * **Paid tier:** click Subscribe/Purchase. Paid access runs on wallet: top up at **Console → Wallet** ($5–$1000 via Stripe), and the price deducts automatically. * **Rules:** one active purchase per buyer per app; upgrades allowed, downgrades not; meter resets on a rolling 30-day window. *** ## Making Requests (stateless MCP 2026-07-28) [#making-requests-stateless-mcp-2026-07-28] All app requests go through the Voidnet at a single endpoint. ``` POST https://api.openvoidnet.com/v1-beta/{adapter}/{username}/{appname} ``` | Parameter | Description | Example | | ---------- | ------------------------------- | --------------- | | `adapter` | Protocol adapter type (`mcp`) | `mcp` | | `username` | Publisher's public username | `acmecorp` | | `appname` | App name (unique per publisher) | `weather-tools` | | Header | Required | Description | | ------------------------------------ | -------- | -------------------------------------------------------- | | `Authorization: Bearer ` | Yes | Your API key (`vnb-sk-*`) or JWT access token (`eyJ...`) | | `Content-Type: application/json` | Yes | Request body format | | `MCP-Protocol-Version` | Yes | Must be `2026-07-28` | | `Mcp-Method` | Yes | JSON-RPC method name (e.g., `tools/call`) | | `Mcp-Name` | Yes | Tool/resource/prompt name (e.g., `get_forecast`) | **Stateless MCP 2026-07-28** — No sessions, no initialize handshake, no `Mcp-Session-Id` header, no GET/DELETE endpoints. SSE streams are per-request on POST; close = cancel. ### Request body [#request-body] The body is a standard JSON-RPC 2.0 request. The `_meta` object in `params` may include `protocolVersion`, `clientInfo`, `clientCapabilities`. ```json { "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } } ``` ### Complete example (API key) [#complete-example-api-key] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_forecast" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } }' ``` ### Complete example (JWT access token) [#complete-example-jwt-access-token] ```bash TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_forecast" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } }' ``` *** ## MCP Methods [#mcp-methods] The apps you query speak the MCP protocol (version `2026-07-28`). Here are the methods you can call: ### server/discover (recommended first) [#serverdiscover-recommended-first] Discover the server's capabilities and supported versions: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: server/discover" \ -H "Mcp-Name: server/discover" \ -d '{"jsonrpc":"2.0","id":"1","method":"server/discover"}' ``` Response includes `supportedVersions`, `capabilities`, `serverInfo`, `instructions`. ### tools/list [#toolslist] Discover what tools an app provides: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/list" }' ``` Response includes `tools` array with `ttlMs` and `cacheScope` for spec-sanctioned caching. ### tools/call [#toolscall] Execute a tool with arguments: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_forecast" \ -d '{ "jsonrpc": "2.0", "id": "2", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } }' ``` Response includes `resultType` (`complete` or `input_required` for MRTR). ### prompts/list [#promptslist] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: prompts/list" \ -H "Mcp-Name: prompts/list" \ -d '{"jsonrpc":"2.0","id":"3","method":"prompts/list"}' ``` ### prompts/get [#promptsget] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: prompts/get" \ -H "Mcp-Name: prompts/get" \ -d '{"jsonrpc":"2.0","id":"4","method":"prompts/get","params":{"name":"weather_summary","arguments":{"location":"London"}}}' ``` ### resources/list [#resourceslist] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: resources/list" \ -H "Mcp-Name: resources/list" \ -d '{"jsonrpc":"2.0","id":"5","method":"resources/list"}' ``` ### resources/read [#resourcesread] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: resources/read" \ -H "Mcp-Name: resources/read" \ -d '{"jsonrpc":"2.0","id":"6","method":"resources/read","params":{"uri":"weather://London/current"}}' ``` ### subscriptions/listen (optional) [#subscriptionslisten-optional] For servers that support change notifications: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: subscriptions/listen" \ -H "Mcp-Name: subscriptions/listen" \ -d '{"jsonrpc":"2.0","id":"7","method":"subscriptions/listen","params":{"notifications":[]}}' ``` *** ## OAuth Endpoints [#oauth-endpoints] The Voidnet exposes the following OAuth 2.0 endpoints: | Endpoint | Description | | --------------------------------------------- | ------------------------------------------------------------------------------ | | `POST /oauth/token` | Issue access tokens (client\_credentials, authorization\_code, refresh\_token) | | `GET /authorize` | OAuth 2.1 authorization code + PKCE (interactive) | | `POST /authorize` | Process login+consent, issue authorization code | | `GET /.well-known/oauth-authorization-server` | RFC 8414 AS metadata | | `GET /.well-known/oauth-protected-resource` | RFC 9728 resource metadata | | `GET /.well-known/jwks.json` | Voidnet's public signing keys | ### AS metadata (RFC 8414) [#as-metadata-rfc-8414] ```json { "issuer": "https://api.openvoidnet.com", "authorization_endpoint": "https://api.openvoidnet.com/authorize", "token_endpoint": "https://api.openvoidnet.com/oauth/token", "jwks_uri": "https://api.openvoidnet.com/.well-known/jwks.json", "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"], "token_endpoint_auth_methods_supported": ["none", "client_secret_post"], "scopes_supported": ["mcp:tools", "llm:completions"], "response_types_supported": ["code"], "code_challenge_methods_supported": ["S256"] } ``` ### Protected resource metadata (RFC 9728) [#protected-resource-metadata-rfc-9728] ```json { "authorization_servers": ["https://api.openvoidnet.com"], "scopes_supported": ["mcp:tools", "llm:completions"], "bearer_methods_supported": ["authorization_header"] } ``` These endpoints let OAuth-aware clients configure themselves automatically without hardcoding token endpoints or signing keys. *** ## Error Handling [#error-handling] The Voidnet returns errors in one JSON shape. The `error` field is an **object**, not a string: ```json { "error": { "code": "error_code", "message": "Human-readable description", "status": 429 } } ``` For the complete catalog, see the [Error Reference](https://docs.openvoidnet.com/docs/error-reference). *** ## Rate Limits & Metering [#rate-limits--metering] The Voidnet enforces a per-minute/per-day **rate limit** and a monthly **meter limit** per app. Exceeding either returns a `429`: * `rate_limit_exceeded` — too fast. Wait for the window to reset and retry with backoff. * `meter_limit_exceeded` — monthly quota exhausted. Wait for the rolling 30-day reset or upgrade your tier. * `meter_expired` — billing period ended. Wait for the meter to reset. There are no `X-RateLimit-*` response headers. Track your remaining quota in the Console under **Usage**. *** ## Calling Apps from Code [#calling-apps-from-code] ### TypeScript / JavaScript [#typescript--javascript] ```typescript const API_KEY = "vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"; const TOKEN_ENDPOINT = "https://api.openvoidnet.com/oauth/token"; const GATEWAY = "https://api.openvoidnet.com"; const USERNAME = "acmecorp"; const APPNAME = "weather-tools"; const COMMON_HEADERS = { "MCP-Protocol-Version": "2026-07-28", "Content-Type": "application/json" }; // Option A: API key (simple) async function callWithKey() { const response = await fetch(`${GATEWAY}/v1-beta/mcp/${USERNAME}/${APPNAME}`, { method: "POST", headers: { "Authorization": `Bearer ${API_KEY}`, ...COMMON_HEADERS, "Mcp-Method": "tools/call", "Mcp-Name": "get_forecast" }, body: JSON.stringify({ jsonrpc: "2.0", id: "1", method: "tools/call", params: { name: "get_forecast", arguments: { location: "London", days: 3 } } }) }); return await response.json(); } // Option B: Get an access token first (production) async function getAccessToken(): Promise { const resp = await fetch(TOKEN_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: API_KEY }) }); const data = await resp.json(); return data.access_token; } async function callWithToken() { const token = await getAccessToken(); const response = await fetch(`${GATEWAY}/v1-beta/mcp/${USERNAME}/${APPNAME}`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, ...COMMON_HEADERS, "Mcp-Method": "tools/list", "Mcp-Name": "tools/list" }, body: JSON.stringify({ jsonrpc: "2.0", id: "1", method: "tools/list" }) }); return await response.json(); } ``` ### Python [#python] ```python import requests API_KEY = "vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" GATEWAY = "https://api.openvoidnet.com" USERNAME = "acmecorp" APPNAME = "weather-tools" COMMON_HEADERS = { "MCP-Protocol-Version": "2026-07-28", "Content-Type": "application/json" } # Option A: API key (simple) response = requests.post( f"{GATEWAY}/v1-beta/mcp/{USERNAME}/{APPNAME}", headers={ "Authorization": f"Bearer {API_KEY}", **COMMON_HEADERS, "Mcp-Method": "tools/call", "Mcp-Name": "get_forecast" }, json={ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": {"location": "London", "days": 3} } } ) data = response.json() print(data["result"]) # Option B: Get an access token first (production) token_resp = requests.post( "https://api.openvoidnet.com/oauth/token", data={ "grant_type": "client_credentials", "client_id": API_KEY } ) token = token_resp.json()["access_token"] response = requests.post( f"{GATEWAY}/v1-beta/mcp/{USERNAME}/{APPNAME}", headers={ "Authorization": f"Bearer {token}", **COMMON_HEADERS, "Mcp-Method": "tools/list", "Mcp-Name": "tools/list" }, json={"jsonrpc": "2.0", "id": "1", "method": "tools/list"} ) ``` ### cURL (reusable) [#curl-reusable] ```bash GATEWAY="https://api.openvoidnet.com" API_KEY="vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" USERNAME="acmecorp" APPNAME="weather-tools" # Authenticate with API key directly curl -X POST "$GATEWAY/v1-beta/mcp/$USERNAME/$APPNAME" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/list" }' # Or exchange for a JWT access token first TOKEN=$(curl -s -X POST "$GATEWAY/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=$API_KEY" | jq -r '.access_token') curl -X POST "$GATEWAY/v1-beta/mcp/$USERNAME/$APPNAME" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}' ``` *** ## FAQ [#faq] ### Do I need to purchase an app before calling it? [#do-i-need-to-purchase-an-app-before-calling-it] Yes — even free tiers require a purchase record. Click "Get" in the marketplace to activate a free app. This creates the necessary record for the Voidnet to authorize your requests. ### How do I find which apps I've purchased? [#how-do-i-find-which-apps-ive-purchased] Go to the **Usage** page in your Console dashboard under the **Buyer** section. You'll see all apps you've purchased, their tier status, API call counts, and remaining meter usage. ### Can I use the same API key for multiple apps? [#can-i-use-the-same-api-key-for-multiple-apps] Yes — a single API key works across all apps you purchase. ### How do I know which methods an app supports? [#how-do-i-know-which-methods-an-app-supports] Call `server/discover` (recommended first) or `tools/list`, `prompts/list`, `resources/list` to discover capabilities. ### When should I use a JWT access token instead of my API key? [#when-should-i-use-a-jwt-access-token-instead-of-my-api-key] Use JWT access tokens in production systems, CI/CD pipelines, and shared environments. They're short-lived (default 1 hour), reducing the risk of credential exposure. Use raw API keys for local development and testing. ### Can I use both API keys and JWT tokens? [#can-i-use-both-api-keys-and-jwt-tokens] Yes — the Voidnet accepts both. OAuth access tokens (`eyJ...`) and API keys (`vnb-sk-*`) are both valid in the `Authorization: Bearer` header. The Voidnet detects the format automatically. ### What happens when my access token expires? [#what-happens-when-my-access-token-expires] The Voidnet returns `api_key_expired` (401). Request a new token from `POST /oauth/token` using your API key (`client_credentials`) or `refresh_token`, then retry. ### Can my organization use Okta or Entra ID to authenticate? [#can-my-organization-use-okta-or-entra-id-to-authenticate] Not yet. The Voidnet authorization server issues tokens via `client_credentials` (exchange API key for JWT) and `authorization_code` + PKCE (interactive). Enterprise identity federation (jwt-bearer) is not currently supported. # MCP OAuth (/docs/buyer-guide/oauth) The Voidnet acts as an OAuth 2.0 authorization server, issuing JWT access tokens for production systems. ## Token types [#token-types] ### JWT access tokens [#jwt-access-tokens] Access tokens are JSON Web Tokens (JWTs) signed with RS256. They are three base64url-encoded segments separated by dots: ``` eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature ``` Each token contains: * **`sub`** — Buyer UUID * **`scope`** — Permission scope (e.g. `mcp:tools`) * **`exp`** — Expiration time (default 1 hour) * **`iat`** — Issued at time * **`iss`** — Voidnet issuer URL * **`aud`** — Voidnet issuer URL (fixed at mint time; no client-controlled audience input) * **`kid`** — Key ID identifying the signing key in the JWKS ### API keys [#api-keys] The Voidnet also accepts API keys (`vnb-sk-*`) as a simpler alternative. See the [Buyer Guide](https://docs.openvoidnet.com/docs/buyer-guide/mcp) for API key management. ## Grant types [#grant-types] ### client\_credentials [#client_credentials] Exchange your API key for a short-lived JWT: ```bash curl -X POST https://api.openvoidnet.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" ``` Successful response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600, "scope": "mcp:tools" } ``` The returned `access_token` is a JWT usable in any `Authorization: Bearer` header. **Notes:** * No separate `client_secret` is needed — the API key itself is the secret * An optional `scope` parameter may be provided (defaults to `mcp:tools`) * The `aud` claim is always the Voidnet issuer URL — the token endpoint accepts no client-controlled audience input ## Using tokens [#using-tokens] All requests to the Voidnet use the `Authorization: Bearer` header: ```bash # API key curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{"jsonrpc": "2.0", "id": "1", "method": "tools/list"}' # JWT access token TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{"jsonrpc": "2.0", "id": "1", "method": "tools/list"}' ``` The Voidnet auto-detects the credential type — three dot-separated segments means JWT, otherwise API key. ### Token expiration [#token-expiration] Tokens have an `exp` claim. When a token expires, the Voidnet returns `api_key_expired` (401): 1. Detect the `401` response 2. Request a new token from `POST /oauth/token` 3. Retry the request with the new token ## Publisher authentication [#publisher-authentication] When the Voidnet proxies a request to a publisher's server, it authenticates using the publisher's API key (`vnp-sk-*`): ``` Authorization: Bearer vnp-sk-27fc6268dc64a9ba2c4cb92489e9175cbf404e260beb268b976f1a56582eff3 ``` Publisher keys are: * Generated in the Console when publishing an app * The Voidnet always sends `vnp-sk-*` regardless of how the buyer authenticated * Sent to the publisher server on every proxied request ## Endpoints [#endpoints] ## OAuth endpoints [#oauth-endpoints] The Voidnet acts as an OAuth 2.0 authorization server: | Endpoint | Description | | --------------------------------------------- | ----------------------------------------- | | `POST /oauth/token` | Issue access tokens (client\_credentials) | | `GET /.well-known/oauth-authorization-server` | RFC 8414 AS metadata | | `GET /.well-known/oauth-protected-resource` | RFC 9728 resource metadata | | `GET /.well-known/jwks.json` | Voidnet's public signing keys | ## Credential management [#credential-management] **API keys** are managed through the Voidnet: * **Generate** — Create a new key from the API Keys section * **Revoke** — Immediately invalidate a compromised key * **Rotate** — Generate a new key, update your configuration, revoke the old one **OAuth access tokens** are obtained programmatically: * **client\_credentials** — Exchange an API key for a JWT at `POST /oauth/token` * Tokens expire after a configurable period (default 1 hour) ## Credential security best practices [#credential-security-best-practices] * Store API keys and tokens in environment variables, not in code * Use a secrets manager in production * Rotate API keys periodically * Revoke compromised keys immediately * Never expose credentials in client-side code, logs, or version control * Use short-lived JWT access tokens in production systems instead of raw API keys # Rate Limiting (/docs/buyer-guide/rate-limiting) The Voidnet enforces usage limits so that no single buyer or key can overload the system or a publisher's server, and so publishers can offer free and paid tiers with monthly quotas. ## Limits [#limits] Your request volume is capped in two ways: * **Rate limit** — a per-minute and per-day cap on how fast you can send requests. Exceeding it returns `429 rate_limit_exceeded`. * **Meter limit** — a monthly quota on total requests per app. Defined by the publisher per tier. Exceeding it returns `429 meter_limit_exceeded`. Meters reset on a rolling 30-day window from your purchase date. When a billing period ends, the gateway returns `429 meter_expired` until the meter resets. ## What happens when you hit a limit [#what-happens-when-you-hit-a-limit] A `429` response looks like: ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded: too many requests per minute", "status": 429 } } ``` ```json { "error": { "code": "meter_limit_exceeded", "message": "Monthly meter limit exceeded", "status": 429 } } ``` There are no `X-RateLimit-*` response headers. Track your usage and remaining quota in the Console under **Usage**. ## Meter limits by tier [#meter-limits-by-tier] | Tier | Typical monthly meter | Purpose | | ---- | --------------------------------- | -------------------- | | Free | 1,000 requests | Trial and evaluation | | Paid | Set by publisher (often 100,000+) | Production usage | Publishers configure these limits when they set up an app's tiers. To see an app's limits, check its marketplace page. ## Best practices [#best-practices] 1. **Implement exponential backoff** — on a `429`, wait before retrying. Start at 1 second, double each attempt. 2. **Track usage in the Console** — the **Usage** page shows remaining meter quota per app so you can anticipate limits. 3. **Distribute across keys** — for high-volume applications, use multiple API keys to spread load. 4. **Upgrade your tier** — if you consistently hit limits, a paid tier from the publisher gives a higher meter. # CLI Output Contract (/docs/cli/output) This page is a contract between the CLI and anything that parses it: shell scripts, CI pipelines, and AI coding agents. Behavior here is asserted by tests; changes to it are breaking changes. ## Streams [#streams] * **stdout carries data only.** Tables, JSON, IDs, rotated secrets. Everything on stdout is safe to pipe. * **stderr carries diagnostics and errors.** Env identity, request lines, row counts, warnings, failures. Humans read it; scripts redirect it away. * `--help`, `--version`, and empty invocation print usage to stdout with no runtime chatter and exit 0. ## JSON envelope [#json-envelope] Every data command accepts `--json` and emits one object: ```json { "data": {} } ``` One shape for every command, no metadata mixed into data. (`apps list` also accepts the legacy `--format=json` spelling; both produce the envelope.) ## Exit codes [#exit-codes] | Code | Meaning | | ---- | ------------------------------------------------------------------------------------------------------------------ | | `0` | Success. For `status` and `doctor`, every probe/check passed. | | `1` | Usage error, auth failure, remote failure, environment failure, or any failed probe/check. stderr names the cause. | | `3` | Reserved: client below minimum supported version (update checker). | ## Prompt discipline [#prompt-discipline] * The CLI never prompts without an interactive terminal. Piped and CI runs fail loud with a non-zero exit instead of hanging. * The CLI never prints full secret values except the two once-only deliveries (`keys rotate`, token creation), each labeled copy-once. * Errors are named shapes (`insufficient_scope`, `insufficient_binding`, `invalid_grant`), never generic strings. Retry logic keys off codes: scope and binding failures never resolve by retrying; grant failures need a fresh login. ## Modes [#modes] `--json` > `--quiet` > `--verbose` > default. `--quiet` shows errors only. `--verbose` restores request URLs, row counts, probe chatter, and the target line. Default prints clean tables; failures always name their target. # Voidnet CLI (/docs/cli/overview) The Voidnet CLI (`voidnet`) manages publisher apps from the terminal. It calls the same publisher API as the Console, using bearer-token auth instead of browser cookies. ```bash npm i -g openvoidnet voidnet login voidnet apps list ``` ## Credentials [#credentials] Voidnet uses three credential types: | Credential | Prefix | Held by | Used for | Accepted at | | --------------------- | ----------- | ------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------- | | Buyer key | `vnb-sk-*` | Buyers and agents | Live tool and chat calls | `https://api.openvoidnet.com/v1-beta/*` | | Per-app publisher key | `vnp-sk-*` | Your server config, one per app | The gateway presents it to your server so incoming calls are verifiable | Validated by your server; no Voidnet endpoint accepts it | | Machine token | `vnp-pub-*` | Terminal, CI, scripts | Publisher management: list apps, read usage, rotate keys, refresh snapshots | `https://openvoidnet.com/api/publisher/v1/*` | Per-app keys travel gateway-to-server. Machine tokens travel client-to-Voidnet. They are not interchangeable. ## Sign-in [#sign-in] **Browser sign-in (recommended):** ```bash voidnet login ``` The CLI starts a one-shot listener on `127.0.0.1`, opens Void Accounts in your browser, and waits up to 5 minutes. After approval, the token is stored at `~/.config/voidnet/cli.json` (mode 600). Approval happens on a consent screen showing the requesting client, the granted permissions in plain language, and an app picker: **All apps** (including apps created later) or **Certain apps** (selected checkboxes). The issued token carries read and verify permissions with a 30-day expiry. Publishing and key rotation require a token created in the Console. To preselect apps: ```bash voidnet login --app "$APP_ID_1" --app "$APP_ID_2" ``` The selection is recorded at approval time and enforced on every call. A token cannot exceed what was approved. **Token sign-in (headless and CI):** ```bash voidnet login --token vnp-pub-... ``` Create the token at Console → Machine Tokens. The full value is shown once at creation. Prefer piping the value over typing it. **Sign out:** ```bash voidnet logout ``` Revokes the token server-side and removes the local config. ## Commands [#commands] ```bash voidnet apps list voidnet apps list --format=json voidnet logs tail --app "$APP_ID" voidnet logs tail --app "$APP_ID" --request-id "$REQUEST_ID" voidnet keys list "$APP_ID" voidnet keys rotate "$APP_ID" ``` `logs tail --request-id` filters Usage Logs by request ID. The gateway forwards the same value as `X-Voidnet-Request-Id` on every routed call, so a publisher's own server logs join to console records on that value. `keys rotate` revokes all keys for the app and prints the new full value once. ## Scopes [#scopes] | Scope | Grants | | --------------------------------- | ---------------------------------------------------------------- | | `apps:read` | List apps and status | | `apps:write` | Create drafts, edit descriptions | | `apps:verify` | Verify servers, refresh snapshots | | `apps:publish` | Publish to marketplace (console-issued tokens only) | | `keys:read` | List key fingerprints | | `keys:rotate` | Revoke app keys and issue a new one (console-issued tokens only) | | `domains:write`, `domains:verify` | Domain verification lifecycle | | `metering:read` | Usage logs with request IDs | | `billing:read` | Payout visibility | Unknown scope strings are rejected. Calls outside a token's scopes fail with `insufficient_scope`; calls outside its bound apps fail with `insufficient_binding`. ## Token rules [#token-rules] * Full values appear once at creation. Lists show name, last 4 characters, scopes, bound apps, expiry, and last use. * Tokens support an app binding: all apps or a fixed list. A compromised token affects only its listed apps. * Suspended publishers lose machine access the same way they lose browser access. * Revoke unused tokens. `last_used_at` on each token shows whether it is active or dormant. ## Troubleshooting [#troubleshooting] | Symptom | Meaning | | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `Timed out waiting for browser approval` | No approval arrived in 5 minutes. Run `voidnet login` again. | | `state mismatch (possible CSRF — aborted)` | The callback did not match this session. Never approve a login you did not start. | | `Invalid grant: code already used` | Each code redeems once. Start a fresh `voidnet login`. | | `no_publisher` | The signed-in account holds no active publisher. Complete publisher onboarding first. | | `insufficient_scope` | The token lacks the operation scope. Browser logins grant read + verify; use a console-issued token for publish and rotate. | ## For AI coding agents [#for-ai-coding-agents] Agents driving this CLI (Claude Code, Cursor, Copilot, Opencode, or any other coding agent) need two things: the machine contract (see [CLI Output Contract](https://docs.openvoidnet.com/docs/cli/output)), and Voidnet platform knowledge. Both agent endpoints below are plain text over HTTPS — any LLM harness fetches them with a single GET, no special protocol needed. Fetch them instead of guessing: * **Map first:** `https://docs.openvoidnet.com/llms.txt` — every page with one-line scope notes. Cheap context; find the right page, then fetch it. * **Full detail when needed:** `https://docs.openvoidnet.com/llms-full.txt` — the complete documentation text. Ground truth for protocol versions, error catalogs, and billing rules. Agent rules that hold for every run: 1. Run `voidnet` against production. Keep credentials in the environment or secret manager, never in files or chat. 2. Parse `--json` output, never tables. Never retry blindly on named errors. 3. Named errors are instructions: `insufficient_scope` means request a console-issued token, `insufficient_binding` means the token lacks that app, `invalid_grant` means start a fresh login. Never retry blindly. 4. Secrets appear once (rotate/create output). Store them in the environment or secret manager immediately; never echo them into logs, plans, or chat. 5. Destructive commands (`keys rotate`, `publish` flows) require explicit human approval in the task — an agent never runs them on inferred intent. ## Updates and host discovery [#updates-and-host-discovery] The CLI checks `/.well-known/voidnet.json` on the management host at most once per day (cached in the OS config dir) and prints a one-line notice on stderr when a newer release exists. Unreachable metadata never fails a command. Versions below the published minimum fail loud with exit code 3 and the upgrade command. Disable with `VOIDNET_NO_UPDATE_CHECK=1` or `--no-update-check`. # Ecosystem (/docs/learn/ecosystem) openvoidnet is the whole: one umbrella for AI apps — tools, models, and agents — plus the products around them that make the marketplace work. One company, one account system, one API, one bill. Under it sit two sides. **Voidnet** is the developer side: builders connect, build, and ship through it. **VoidAI** is the consumer side: powered by Voidnet, no code, no API keys, no setup. Two doors into the same family. The parts, each doing exactly one job: * **Voidnet Console** — the marketplace and dashboards ([openvoidnet.com](https://openvoidnet.com)). * **Voidnet** — the API between buyers and publisher apps ([api.openvoidnet.com](https://api.openvoidnet.com)). * **Void Accounts** — the one login for everything ([accounts.openvoidnet.com](https://accounts.openvoidnet.com)). * **VoidAI** — chat connected to the marketplace and a workspace ([chat.openvoidnet.com](https://chat.openvoidnet.com)). * **Void Share** — public links out of the workspace ([share.openvoidnet.com](https://share.openvoidnet.com)). * **Docs** — this site: the contract, written from the live system. What each part does, in detail, follows. ## Voidnet Console [#voidnet-console] The home of the ecosystem. Buyers discover apps in the Marketplace, purchase access, manage API keys, and track usage and spend. Publishers verify and publish apps, set free and paid tiers, connect Stripe once, and track earnings and payouts. Two ways to buy: free access is one click; paid access goes through the wallet (top up once, spend per call) or Stripe checkout (one-time or subscription). Purchases are per app and permanent — buying what you already own is rejected, never double-billed. Developers meet the marketplace here; everyone else meets the same apps in the Void Store, without the developer parts. Everything human-facing — buying, publishing, paying, reconciling — happens here. ([openvoidnet.com/console](https://openvoidnet.com/console)) ## Voidnet [#voidnet] The API between buyers and publisher apps. Every call is authenticated, checked for access, limited, metered, and billed before the answer returns. One endpoint, adapter in the path, per protocol. Buyers never connect to publisher servers directly, and publishers never see buyer credentials. The gateway stands between them on every call. ([api.openvoidnet.com](https://api.openvoidnet.com)) ## Void Accounts [#void-accounts] One login for everything. Sign-up, sign-in, sessions, two-factor, and social login live here; every other product redirects logins to it. An identity created once works across Console, VoidAI, and Share. Developers enroll once and claim a username, which becomes the publisher identity everywhere. Accounts also mints the API keys developers call through. ([accounts.openvoidnet.com](https://accounts.openvoidnet.com)) ## VoidAI [#voidai] Chat with AI connected to the marketplace. The assistant answers with a model and calls published tools mid-turn — checking data, running tools, quoting models — without leaving the conversation. VoidAI is a buyer like any other: same access rules, same metering, same billing. ([chat.openvoidnet.com](https://chat.openvoidnet.com)) ## Workspace [#workspace] Behind every conversation sits a workspace: files the assistant reads, writes, edits, runs, and shares while it works. Work organizes into areas — Notes, Mindmaps, Docs (Word files the AI builds), Slides, Sites (shareable pages), Codespaces (code plus terminal), per-conversation scratches, and Outputs for finished artifacts. Each area opens a matching surface: paged preview for documents, present mode for slides, sandboxed rendering for sites. The assistant works files inside the turn: listing, reading, drafting, editing, running code, handing back links. Files stay private until shared; shared files are pinned while unshared drafts expire under lifecycle rules. ## How AI uses apps and workspace together [#how-ai-uses-apps-and-workspace-together] One turn can span all three: answer with a marketplace model, call a marketplace tool mid-sentence (`appname.toolname`), write the result into a workspace file, build it into a document or deck, and return a public share link. Bring your own keys (Groq, DeepSeek, OpenRouter, OpenAI) and switch models per conversation, or use the built-in models. ## Void Share [#void-share] Workspace results become public links: no login required to view. Documents preview as pages, slides present fullscreen, sites run isolated, PDFs stream, spreadsheets download. Share links rotate or revoke instantly, and oversized files serve as downloads instead of previews. Shared artifacts link back into the ecosystem. ([share.openvoidnet.com](https://share.openvoidnet.com)) ## Docs [#docs] Guides and references for building on Voidnet Console — this site. Protocols, buyer and publisher guides, billing rules, full error catalog. Docs are written from the live system and versioned with it: what you read is the contract you get. ## For publishers: CLI and SDK [#for-publishers-cli-and-sdk] Work from the Console or the terminal. The `openvoidnet` CLI logs in through the browser, lists apps, rotates keys, and tails live request logs. The JavaScript SDK wraps a tool server in one call, including the domain-verification file route. ## How all work together [#how-all-work-together] A buyer signs up in Accounts, gets a key, finds an app in Console, gets access, and calls it through the Voidnet. A publisher enrolls in Accounts, verifies a domain and publishes in Console, serves traffic through the Voidnet, and tracks usage in the same dashboard. No product duplicates another: Console never proxies app traffic, the Voidnet never renders a page, Accounts never touches money, VoidAI never publishes apps, Share never bills. Each request and each dollar crosses each boundary exactly once, in the same order, every time. ## How the ecosystem forms [#how-the-ecosystem-forms] Publishers list capabilities, buyers pay to use them, payouts fund more publishers. Console reduces publishing to a form plus a server URL. The wallet reduces buying to one top-up instead of one checkout per app. VoidAI reduces discovery to a sentence in a chat. Each turn adds supply and demand together: a published app is a reason to sign up, and every signup is an audience for the next app. Share carries artifacts onto the open web, where they return as new users. ## Network effects [#network-effects] More apps make keys and top-ups worth having. More buyers make publishing worth doing. More usage makes payouts larger, which funds better apps. The moat is structural: the purchase graph, usage history, and payout rails grow more valuable with every participant, and none of them transfer by copying code. ## Identity [#identity] Publishers are `@username` — lowercase letters, numbers, and hyphens, 3–20 characters, chosen once at enrollment and never changeable. Every app belongs to exactly one publisher, and every app's public identity is `username/appname`: ``` POST /v1-beta/{adapter}/{username}/{appname} ``` App names are unique per publisher (`a-z, 0-9, -, _`, 3–50 chars). The same pair identifies the app across marketplace pages, purchase records, usage logs, API keys, and gateway routes — MCP tools, LLM models, and beyond. The full system — users, developers, tools, models, agents, keys, and the trust chain — is [Identity](https://docs.openvoidnet.com/docs/learn/identity). ## Composability [#composability] One gateway, one credential shape, one meter — so apps compose. An assistant answers with a model and reaches for tools mid-sentence. An outside product calls a marketplace model for language and marketplace tools for everything else. No special integration: if an app can be called, it can be combined. ## Developer resources [#developer-resources] * [Void Apps](https://docs.openvoidnet.com/docs/learn/void-apps) — app types and how they are consumed and published. * [Voidnet](https://docs.openvoidnet.com/docs/learn/voidnet) — the API contract: one endpoint, errors, auth. * [Identity](https://docs.openvoidnet.com/docs/learn/identity) — the one identity system behind users, developers, tools, models, and agents. * [Get Started](https://docs.openvoidnet.com/docs/get-started) — credentials and your first call. * [API Reference](https://docs.openvoidnet.com/docs/reference/api-reference) · [Error Reference](https://docs.openvoidnet.com/docs/reference/error-reference). * Code: [GitHub](https://github.com/openvoidnet/) · [npm](https://www.npmjs.com/package/openvoidnet) · [PyPI](https://pypi.org/project/openvoidnet/). # Identity (/docs/learn/identity) Every actor and every workload on Voidnet has exactly one identity, and all of them work the same way. Learn the pattern once; it never varies. ## Users [#users] A user is an email plus a unique username. Usernames are lowercase letters, numbers, and hyphens, 3–20 characters. Users carry roles — every account starts as a user, and enrolling as a developer adds the developer role. ## Developers and publishers [#developers-and-publishers] Claiming a username as a developer creates your publisher identity: one user, one publisher, and the username never changes. Your publisher name is your public face across the ecosystem — marketplace listings, purchase records, usage logs, and API routes all render it the same way, `@username`. ## Domains [#domains] Publishing requires a verified domain, and one domain belongs to one account — a domain verified anywhere cannot be verified again elsewhere. See [Domain Verification](https://docs.openvoidnet.com/docs/domain-verification) for the file, the checks, and reading failures. ## Apps [#apps] Every app belongs to exactly one publisher, and its public identity is `username/appname`: ``` POST /v1-beta/{adapter}/{username}/{appname} ``` App names are unique per publisher (`a-z, 0-9, -, _`, 3–50 characters). The same pair identifies the app everywhere: marketplace pages, purchases, usage, keys, and gateway routes — across MCP tools, LLM models, and beyond. ## Tools [#tools] A tool's identity is its app plus its tool name, dotted: `appname.toolname`. When an assistant calls a tool, that dotted name is the whole address — it resolves to exactly one tool, on exactly one app, from exactly one publisher. ## Models [#models] An LLM app carries exactly one model, addressed by its exact published model name. Text in, text out — the name you call is the name that was verified. ## Agents [#agents] Agents follow the same publisher-scoped identity as every other workload: owned by one publisher, addressed through the same `username/appname` shape, metered and keyed like everything else. ## Keys [#keys] Keys are prefixed so their kind is visible at a glance: buyer keys start with `vnb-sk-`, publisher server keys with `vnp-sk-`. Keys are scoped — a buyer key spends that buyer's wallet on purchased apps, a publisher key is valid only for its own app server. ## The trust chain [#the-trust-chain] Domain proves publisher. Publisher owns apps. Apps expose tools and models. Buyers purchase apps and call them with scoped keys. Every link is checkable from the one before it — that chain is what "identity" means on Voidnet. ## Further reading [#further-reading] * [Ecosystem](https://docs.openvoidnet.com/docs/learn/ecosystem) — how the pieces fit together. * [Void Apps](https://docs.openvoidnet.com/docs/learn/void-apps) — app types, consuming and publishing. * [Domain Verification](https://docs.openvoidnet.com/docs/domain-verification) — proving domain ownership. * [MCP Publisher](https://docs.openvoidnet.com/docs/mcp/publisher) · [LLM Publisher](https://docs.openvoidnet.com/docs/llm/publisher) — shipping tools and models. # Void Apps (/docs/learn/void-apps) Void Apps are services published by developers and made available to buyers through the marketplace. The Voidnet routes buyer requests to the publisher's server, handling authentication, rate limiting, metering, and billing. Your code only talks to the gateway. ## App types [#app-types] ### MCP — AI Tools (available) [#mcp--ai-tools-available] [Model Context Protocol](https://modelcontextprotocol.io) servers expose callable tools, data resources, and prompt templates. AI clients discover and invoke these tools at runtime. Protocol version `2026-07-28` (stateless). An MCP server can expose: * **Tools** — callable functions with typed JSON input schemas. * **Resources** — URI-addressable data. * **Prompts** — reusable prompt templates. Available today: MCP tools and AI models. One model per LLM app. ### LLM — Language Model Integration (available) [#llm--language-model-integration-available] Connect and query language model providers through a unified OpenAI-compatible interface. **TEXT only** — no image, audio, or video. One model per app, token-metered (`usage.total_tokens`), billed per token. Host with vLLM/Ollama at `https://your-host` and expose `POST https://your-host/v1/chat/completions`. Example: `HuggingFaceTB/SmolLM2-135M-Instruct` on `http://localhost:6010/v1` or Ollama `http://localhost:11434/v1`. ## How apps are consumed [#how-apps-are-consumed] ### MCP (stateless 2026-07-28) [#mcp-stateless-2026-07-28] ``` POST /v1-beta/mcp/{username}/{appname} Authorization: Bearer vnb-sk-... # API key Authorization: Bearer eyJhbGciOiJSUzI1... # JWT access token Headers: MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: get_forecast ``` ### LLM (OpenAI compatible) [#llm-openai-compatible] ``` POST /v1-beta/llm/{username}/{appname} # gateway Authorization: Bearer vnb-sk-... # buyer key Content-Type: application/json Body: {"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"hi"}],"temperature":0.7,"max_tokens":64,"stream":false} # With OpenAI SDK: # from openai import OpenAI; client = OpenAI(base_url="https://api.openvoidnet.com/v1-beta/llm/{username}/{appname}", api_key="vnb-sk-...") # client.chat.completions.create(model="...", messages=[{"role":"user","content":"hi"}]) ``` 1. Browse the Marketplace and purchase an app (free tiers require clicking "Get"). 2. Note the publisher's **username** and the app's **name**. 3. Send requests through the gateway — JSON-RPC 2.0 for MCP (above), a chat-completions body for LLM (above). **Stateless MCP 2026-07-28** — No sessions, no initialize handshake, no `Mcp-Session-Id` header, no GET/DELETE endpoints. SSE streams are per-request on POST; close = cancel. The gateway accepts both API keys and JWT access tokens and auto-detects the credential type. ## How apps are published [#how-apps-are-published] Publishers register their app, choose its type (`mcp` or `llm`), verify a domain ([Domain Verification](https://docs.openvoidnet.com/docs/domain-verification)), and provide their server URL (`https://host/v1` for LLM, `https://host/mcp` for MCP). The gateway stores the mapping between `username/appname` and the server. When a buyer makes a request, the gateway resolves the server URL, authenticates, validates the purchase, and proxies the request. LLM apps are one model per app — the published model ID must match the `model` buyers send. Publishers who want to build and list an app should read the [Publisher Guide (MCP)](https://docs.openvoidnet.com/docs/mcp/publisher) or [Publisher Guide (LLM)](https://docs.openvoidnet.com/docs/llm/publisher). # Voidnet (/docs/learn/voidnet) The Voidnet is the central routing layer of the Voidnet Console platform. You send it a request; it authenticates you, enforces your subscription and usage limits, and proxies the request to the publisher's server. You never connect to a publisher directly. ``` Buyer (API key or JWT) → Voidnet → Publisher's server ``` ## What the gateway does [#what-the-gateway-does] 1. **Authenticates** your credential (API key or JWT). 2. **Checks your purchase** of the app and your tier's meter. 3. **Enforces rate limits** (per-minute, per-day, monthly meter). 4. **Routes** the request to the publisher's server. 5. **Returns** the publisher's response, or a gateway error if something fails. You do not need to know the publisher's server URL. The gateway resolves it from the `username/appname` in your request path — see [Identity](https://docs.openvoidnet.com/docs/learn/identity) for the naming rules behind the pair. ## Endpoint [#endpoint] All app requests go through one endpoint. The adapter in the path selects the protocol: ``` POST /v1-beta/{adapter}/{username}/{appname} ``` | Parameter | Value | | ---------------------- | --------------------------------------------------------------------------------- | | `adapter` | `mcp` or `llm` — see [Void Apps](https://docs.openvoidnet.com/docs/learn/void-apps) | | `username` | Publisher's public username | | `appname` | App name, unique per publisher | | `Authorization` | `Bearer `, or `Bearer ` (scope `mcp:tools` or `llm:completions`) | | `MCP-Protocol-Version` | `2026-07-28` (required for `mcp` only) | | `Mcp-Method` | JSON-RPC method name (required for `mcp` only) | | `Mcp-Name` | Tool/resource/prompt name (required for `mcp` only) | **MCP — Stateless 2026-07-28:** * Only `POST` is supported. `GET` and `DELETE` return `405 Method Not Allowed`. * SSE streams are per-request on POST; close = cancel. No `Last-Event-ID`, no `Mcp-Session-Id`. **LLM — OpenAI compatible:** ``` POST /v1-beta/llm/{username}/{appname} Authorization: Bearer vnb-sk-... Content-Type: application/json Body: {"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"hi"}],"stream":false} ``` * One model per app — the `model` you send must match the app's published model exactly. * `stream:true` is supported (SSE); the gateway injects `stream_options.include_usage:true` when missing, and streams without a final usage chunk fail loudly instead of going unbilled. Multimodal inputs (`image_url`, `audio`) are rejected — TEXT only. * Your LLM server must expose `POST {serverUrl}/chat/completions` (`{serverUrl}` is your configured Server URL, which already ends in `/v1`) and accept any `Authorization: Bearer ` header. Plus operational endpoints: | Method | Path | Purpose | | ------ | ----------------------------------------- | ---------------------------------------------------------------------------------- | | `GET` | `/health` | Health check (db + redis) | | `POST` | `/oauth/token` | Issue JWT access tokens (client\_credentials, authorization\_code, refresh\_token) | | `GET` | `/.well-known/oauth-authorization-server` | OAuth metadata (RFC 8414) | | `GET` | `/.well-known/oauth-protected-resource` | OAuth metadata (RFC 9728) | | `GET` | `/.well-known/jwks.json` | Public signing keys | | `GET` | `/authorize` | OAuth 2.1 authorization code + PKCE, login and consent pages (interactive clients) | | `POST` | `/authorize` | Submit login + consent, issue authorization code (interactive clients) | For the full request/response schema of every operation, see the [API Reference](https://docs.openvoidnet.com/docs/reference/api-reference) — it is generated from the gateway source and cannot drift. ## Authentication [#authentication] Two credential types, both in the `Authorization: Bearer` header. The gateway auto-detects which you're using: * **API keys** (`vnb-sk-*`) — long-lived, generated in the Console. * **JWT access tokens** — short-lived (1 hour), obtained from `POST /oauth/token`. Use these in production. A 3-segment (dot-separated) value is treated as a JWT; a `vnb-sk-` prefix is treated as an API key. ## Errors [#errors] All gateway errors use one shape (OAuth endpoints are the exception — see [Error Reference](https://docs.openvoidnet.com/docs/reference/error-reference#error-reference)): ```json { "error": { "code": "error_code", "message": "Human-readable description", "status": 429 } } ``` # LLM Buyer Guide (/docs/llm/buyer) Call a publisher's AI model through Voidnet. One model per app, billed per token. Use the OpenAI SDK — just change the `base_url`. ## Credentials [#credentials] Use an API key from **Voidnet Console → API Keys** (`vnb-sk-*`) or a short-lived JWT from `POST /oauth/token` (`grant_type=client_credentials`, `scope=llm:completions`). The gateway accepts both. ## Find an app [#find-an-app] Browse the **Voidnet Marketplace** and note the publisher's username, the app name, and the model ID listed on the app page (for example `Qwen/Qwen2.5-7B-Instruct`). Each app serves exactly one model — the `model` in your request must match it. ## Get access [#get-access] Apps offer free tiers, paid tiers, or both. Purchase in the Marketplace to gain access: * **Free tier** — tap Get Free. Access is instant, no payment. * **Paid tier** — tap Subscribe/Purchase. Paid access runs on wallet: the price is deducted from your wallet balance (one-time/setup charge at grant, plus per-token charges per call on usage-based apps). Top up first if your balance is short. * **Upgrade** — own free and want paid? The app page shows an **Upgrade** button that moves you to the paid tier. Your free access is revoked automatically when the paid grant completes. There is exactly one active tier per app. Top up at **Voidnet Console → Wallet**: $5–$1000 per top-up via Stripe Checkout. You return to the wallet page and the balance credits automatically. ## Call it [#call-it] ### cURL [#curl] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm \ -H "Authorization: Bearer vnb-sk-a1b2c3..." \ -H "Content-Type: application/json" \ -d '{"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"Write a haiku"}],"temperature":0.7,"max_tokens":64,"stream":false}' ``` Response (OpenAI-compatible): ```json { "id": "chatcmpl-abc", "object": "chat.completion", "created": 1730000000, "model": "Qwen/Qwen2.5-7B-Instruct", "choices": [{"index":0,"message":{"role":"assistant","content":"An old pond..."},"finish_reason":"stop"}], "usage": {"prompt_tokens":10,"completion_tokens":12,"total_tokens":22} } ``` ### Streaming [#streaming] `stream:true` is supported (SSE, OpenAI shape). The gateway injects `stream_options.include_usage:true` when missing so the call stays metered. A stream whose publisher never sends a final usage chunk fails loudly instead of going unbilled. ```bash curl -X POST https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm \ -H "Authorization: Bearer vnb-sk-a1b2c3..." \ -H "Content-Type: application/json" \ -d '{"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"hi"}],"stream":true}' ``` ### OpenAI SDK [#openai-sdk] ```python from openai import OpenAI client = OpenAI(base_url="https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm", api_key="vnb-sk-a1b2c3...") response = client.chat.completions.create(model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role":"user","content":"hi"}]) print(response.choices[0].message.content, response.usage.total_tokens) ``` ```ts import OpenAI from "openai" const openai = new OpenAI({ baseURL: "https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm", apiKey: "vnb-sk-..." }) const response = await openai.chat.completions.create({ model: "Qwen/Qwen2.5-7B-Instruct", messages: [{role:"user",content:"hi"}] }) ``` The only changes from a standard OpenAI call are the `baseURL` (your app's gateway URL) and the `model` (must match the app's published model). ## Models listing [#models-listing] A read-only listing of the publisher's models (proxied from their server, OpenAI shape): ```bash curl https://api.openvoidnet.com/v1-beta/llm/acmecorp/my-llm/models \ -H "Authorization: Bearer vnb-sk-a1b2c3..." ``` Non-billable — no metering, no wallet charge. Full interactive reference (parameters, responses, Try-it) for every LLM operation: [LLM API](https://docs.openvoidnet.com/docs/reference/api/llm). ## Billing [#billing] Paid LLM apps price per 1M tokens (shown as $X/1M on the app page). Each call deducts `total_tokens / 1M × price` from your wallet; an empty wallet returns `402 insufficient_balance`. Track spending per app in **Voidnet Console → Usage**. ## Errors you will see [#errors-you-will-see] | Status | Code | Meaning | | ------ | ------------------------------------- | ------------------------------------------------------------------------------------------- | | 401 | `api_key_invalid` / `api_key_missing` | Bad or missing credential | | 403 | `app_not_purchased` | No live purchase — get the free tier or subscribe first | | 404 | `app_not_found` | Wrong publisher username or app name | | 400 | `invalid_request` | Missing `model`/`messages`, bad JSON, or multimodal content (`image_url`, `audio`, `video`) | | 4xx | `publisher_client_error` | The publisher's server rejected the request (for example unknown model) — body preserved | | 429 | `rate_limit_exceeded` | Too fast — back off and retry (see [Rate Limiting](https://docs.openvoidnet.com/docs/reference/rate-limiting)) | | 429 | `meter_limit_exceeded` | Monthly tier quota used — wait for reset or upgrade tier | | 402 | `insufficient_balance` | Wallet too low for a paid call — top up in Console → Wallet | | 502 | `publisher_error` | Publisher offline, invalid JSON, or stream without a usage chunk | ## Limits [#limits] * TEXT only — `content` must be a string. `image_url`, `audio`, and `video` are rejected. * Only `chat/completions` for inference, plus the read-only models listing above — no embeddings. * Single model per app. # LLM Overview (/docs/llm/overview) LLM apps serve exactly **one AI model** each through an OpenAI-compatible endpoint. Billed per token from `usage.total_tokens`. ## At a glance [#at-a-glance] | | | | ------------ | ------------------------------------------------------------------------------------------ | | Endpoint | `POST /v1-beta/llm/{username}/{appname}` | | Body | `{"model":"","messages":[{...}],"stream":false}` | | Billing unit | Tokens (`total_tokens / 1M × price`) | | Start here | [Buyer Guide](https://docs.openvoidnet.com/docs/llm/buyer) to call · [Publisher Guide](https://docs.openvoidnet.com/docs/llm/publisher) to publish | ## Rules that bite [#rules-that-bite] * The `model` you send must match the app's published model exactly — a read-only listing is available at `GET /v1-beta/llm/{username}/{appname}/models` (see [Buyer Guide](https://docs.openvoidnet.com/docs/llm/buyer)). * TEXT only: `image_url`, `audio`, `video` are rejected before reaching the publisher. * `stream:true` is supported (SSE); the gateway injects `stream_options.include_usage:true` when missing. Streams that never send a final usage chunk fail `502` instead of going unbilled. * Publishers: serve `POST {serverUrl}/chat/completions` (`{serverUrl}` is your configured Server URL, which already ends in `/v1`), return `usage.total_tokens` greater than zero on every success. # LLM Publisher Guide (/docs/llm/publisher) Publish an AI model as a Void App. One model per app, billed per token. Your server just needs to speak OpenAI-compatible `POST /v1/chat/completions`. ## Quick start [#quick-start] Use any OpenAI-compatible server — vLLM, Ollama, or similar: ```bash # Ollama ollama pull smollm2:135m curl http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" \ -d '{"model":"smollm2:135m","messages":[{"role":"user","content":"hi"}],"stream":false}' # vLLM vllm serve HuggingFaceTB/SmolLM2-135M-Instruct --host 0.0.0.0 --port 9000 --served-model-name smollm2:135m curl http://localhost:9000/v1/chat/completions -H "Content-Type: application/json" \ -d '{"model":"smollm2:135m","messages":[{"role":"user","content":"hi"}]}' ``` Both return: ```json {"choices":[{"message":{"content":"Hello"}}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}} ``` ## Publish (Voidnet Console) [#publish-voidnet-console] 1. Verify a domain: **Console → Domains** → follow the verification steps. 2. Connect payouts: **Console → Payments** → connect and activate your Stripe account. **Every paid tier requires an active `stripe_transfers` capability** — publishing a paid tier without it is rejected, because earnings have no payout destination otherwise. 3. Go to **Publish Apps → LLM** and fill in: * **App Name** — unique per publisher `a-z, 0-9, -, _` (3–50 chars) * **Model** — the exact model ID your server serves (for example `HuggingFaceTB/SmolLM-135M-Instruct`) * **Server URL** — `https://your-host/v1` (must end with `/v1`; the gateway will call `/chat/completions` on it) 4. Click **Create Draft** — you will receive a publisher API key. The gateway uses it to authenticate to your server, so your server should accept any `Authorization: Bearer ` header. 5. In the overview click **Publish** to make your app live in the Marketplace. You can configure free tiers, paid tiers, or both, with token limits and pricing during publishing. ## Your server contract [#your-server-contract] Your server must expose a single endpoint: **`POST {serverUrl}/chat/completions`** — `{serverUrl}` is the configured Server URL above (already ends in `/v1`). **`GET {serverUrl}/models`** — expose your OpenAI models listing (vLLM and Ollama serve it natively). The gateway proxies it to buyers at `GET /v1-beta/llm/{username}/{appname}/models`, non-billable.\*\* **Request — the gateway forwards the buyer's JSON verbatim:** ```json {"model":"your-model-id","messages":[{"role":"user","content":"hi"}],"temperature":0.7,"max_tokens":64,"stream":false} ``` `model` and `messages` are required. Multimodal content (`image_url`, `audio`, `video`) is rejected before reaching your server — TEXT only. **Streaming — supported, with one hard rule:** `stream:true` requests reach your server as SSE. Your stream **must** end with a final data chunk carrying `usage` (OpenAI `stream_options.include_usage` shape) before `[DONE]`. The gateway injects `stream_options.include_usage:true` into buyer requests that lack it, but if your server drops it and never sends usage, the call fails `502` instead of going unbilled. vLLM always sends usage; Ollama needs the option. **Response — you must return valid OpenAI shape:** ```json { "id": "chatcmpl-...", "object": "chat.completion", "created": 123, "model": "your-model-id", "choices": [{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}], "usage": {"prompt_tokens":10,"completion_tokens":5,"total_tokens":15} } ``` `total_tokens` must be present and greater than zero — it is the billing input. Return `4xx`/`5xx` with `{"error":{"message":"..."}}` on failure; the gateway maps your status range to `publisher_client_error`/`publisher_server_error` and preserves your body. **Auth:** Accept any `Authorization: Bearer ` header. **Limits:** Response must be under 16MB. ## Tiers and billing [#tiers-and-billing] * Apps are metered per token (`total_tokens` from your response). Your free and paid tier limits are token limits. * Paid LLM apps price **per 1M tokens**. Each buyer call deducts `total_tokens / 1M × price` from the buyer's wallet and credits your earnings in the same atomic step. Fixed-price tiers deduct the full price at purchase instead. * The platform fee defaults to **20%** (`floor(gross × pct / 100)`). * Buyers on a free tier who upgrade to paid are moved automatically: the free purchase is revoked when the paid grant completes. One active tier per buyer per app. ## Payouts [#payouts] Wallet earnings accumulate in the platform ledger and pay out **weekly on Fridays**, one Stripe Transfer per publisher per week covering all your apps and sales, after a 7-day settlement hold. Track earnings, per-app breakdowns, and payout history (week, sales count, transfer id, status) in **Console → Payments**. Legacy Stripe-checkout earnings split automatically at charge time and never enter this ledger. There are no buyer-facing refunds; disputed charges are handled through Stripe. ## Test via gateway [#test-via-gateway] After your app is published and a buyer has purchased it (free or paid): ```bash curl -X POST "https://api.openvoidnet.com/v1-beta/llm/{publisher}/{app}" \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -d '{"model":"your-model-id","messages":[{"role":"user","content":"hello"}]}' ``` Or with the OpenAI SDK — just change the `base_url`: ```python from openai import OpenAI client = OpenAI(base_url="https://api.openvoidnet.com/v1-beta/llm/{publisher}/{app}", api_key="vnb-sk-...") response = client.chat.completions.create(model="your-model-id", messages=[{"role":"user","content":"hi"}]) print(response.choices[0].message.content, response.usage.total_tokens) ``` Streaming test — expect SSE chunks plus a final usage chunk: ```bash curl -X POST "https://api.openvoidnet.com/v1-beta/llm/{publisher}/{app}" \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -d '{"model":"your-model-id","messages":[{"role":"user","content":"hello"}],"stream":true}' ``` ## Limits [#limits] TEXT only, single model per app, no embeddings. `total_tokens` present and greater than zero on every success. Full interactive reference: [LLM API](https://docs.openvoidnet.com/docs/reference/api/llm). See [Buyer Guide (LLM)](https://docs.openvoidnet.com/docs/llm/buyer). # MCP Buyer Guide (/docs/mcp/buyer) This guide covers everything you need to discover, purchase, and query apps published by developers on the Voidnet Console marketplace. New to Voidnet? [Get Started](https://docs.openvoidnet.com/docs/get-started) walks you through credentials and your first call. *** ## Overview [#overview] A **buyer** is a user who discovers apps in the Voidnet Console marketplace and calls them through the Voidnet. The Voidnet handles authentication, rate limiting, metering, and billing — so you only need valid credentials and the app's public name. ``` Your Client (Key or OAuth Token) → Voidnet → Publisher's MCP Server ``` The Voidnet supports three authentication methods: | Method | Credential | Use Case | | ----------------------------------- | ----------------------- | ----------------------------------------------------- | | **API Keys** | `vnb-sk-*` | Simple, long-lived secrets for developers | | **OAuth Client Credentials** | JWT from `/oauth/token` | Production systems, CI/CD | | **OAuth Authorization Code + PKCE** | JWT from `/oauth/token` | Interactive clients (Claude Desktop, Cursor, VS Code) | *** ## Prerequisites [#prerequisites] * A Voidnet Console account ([sign up](https://openvoidnet.com/console)) * An API key for authentication (see below) * Basic understanding of JSON-RPC 2.0 and MCP protocol *** ## Authentication [#authentication] ### API Keys [#api-keys] API keys are the simplest way to authenticate. Generate them from the **API Keys** page under the **Buyer** section of your [Console dashboard](https://openvoidnet.com/console): | Prefix | Type | Use Case | | --------- | ----------------- | --------------- | | `vnb-sk-` | Buyer Service Key | General purpose | ``` vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6 ``` **Security rules:** * Save your key immediately — it's shown only once at creation * Never expose keys in client-side code, version control, or logs * Rotate keys periodically from the Console * Revoke compromised keys immediately ### OAuth Access Tokens [#oauth-access-tokens] For production systems, you can use OAuth 2.0 access tokens instead of raw API keys. The Voidnet runs its own authorization server and supports three grant types: | Grant Type | Use Case | | -------------------- | --------------------------------------------------------------- | | `client_credentials` | Machine-to-machine (API key → short-lived JWT) | | `authorization_code` | Interactive clients (Claude Desktop, Cursor, VS Code) with PKCE | | `refresh_token` | Rotate expired access tokens without re-login | Access tokens are JSON Web Tokens (JWTs) signed with RS256. They expire after a configurable period (default 1 hour) and can be used anywhere you'd use an API key. ### When to use which [#when-to-use-which] | Situation | Recommended | | ------------------------------------------------ | -------------------------------------------- | | Local development, testing | API key (`vnb-sk-*`) | | Production services, CI/CD | JWT access token (client\_credentials) | | Interactive AI clients (Claude, Cursor, VS Code) | Authorization Code + PKCE | | Highly sensitive environments | Rotate API key → short-lived JWT per session | *** ## Getting an Access Token [#getting-an-access-token] ### Client Credentials (machine-to-machine) [#client-credentials-machine-to-machine] Exchange your `vnb-sk-*` key for a JWT access token: ```bash curl -X POST https://api.openvoidnet.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" ``` Successful response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600, "scope": "mcp:tools" } ``` The returned `access_token` is a JWT you can use in the `Authorization` header instead of your raw API key. **Notes:** * The `client_id` is your full API key — no separate `client_secret` is needed * A `scope` parameter may be provided (optional, defaults to `mcp:tools`) * The `aud` claim is always the Voidnet issuer URL — the token endpoint accepts no client-controlled audience input ### Authorization Code + PKCE (interactive clients) [#authorization-code--pkce-interactive-clients] For interactive clients (Claude Desktop, Cursor, VS Code) with a human in the loop: 1. Client discovers metadata from `GET /.well-known/oauth-authorization-server` 2. Client opens browser to `GET /authorize?client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256&response_type=code` 3. User logs in and consents on the gateway-hosted page 4. Gateway redirects to `redirect_uri?code=...&state=...` 5. Client exchanges code for tokens: `POST /oauth/token` with `grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier` 6. Response includes `access_token` (JWT), `refresh_token` (30-day, rotated on use) **Required PKCE parameters:** * `code_challenge` — 43-char base64url (S256 of `code_verifier`) * `code_challenge_method` — must be `S256` * `code_verifier` — 43-128 char unreserved string (used at token exchange) ### Refresh Token (rotate without re-login) [#refresh-token-rotate-without-re-login] ```bash curl -X POST https://api.openvoidnet.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=" \ -d "client_id=vnb-sk-..." ``` Response includes new `access_token` + **rotated** `refresh_token`. Old refresh token is invalidated. ### Token expiration [#token-expiration] Access tokens have an `exp` claim. When a token expires, the Voidnet returns `api_key_expired` (401). You should: 1. Detect the `401` response 2. Request a new token from `/oauth/token` (client\_credentials or refresh\_token) 3. Retry the request with the new token *** ## Apps and Purchasing [#apps-and-purchasing] Browse the [Marketplace](https://openvoidnet.com/marketplace). Each app lists its publisher, name, description, tier (free/paid), and capabilities. To query an app you need the **publisher's username** and **app name** — these form the public route. * **Free tier:** click "Get" to start using immediately (rate limits apply). * **Paid tier:** click Subscribe/Purchase. Paid access runs on wallet: top up at **Console → Wallet** ($5–$1000 via Stripe), and the price deducts automatically. * **Rules:** one active purchase per buyer per app; upgrades allowed, downgrades not; meter resets on a rolling 30-day window. *** ## Making Requests (stateless MCP 2026-07-28) [#making-requests-stateless-mcp-2026-07-28] All app requests go through the Voidnet at a single endpoint. ``` POST https://api.openvoidnet.com/v1-beta/{adapter}/{username}/{appname} ``` | Parameter | Description | Example | | ---------- | ------------------------------- | --------------- | | `adapter` | Protocol adapter type (`mcp`) | `mcp` | | `username` | Publisher's public username | `acmecorp` | | `appname` | App name (unique per publisher) | `weather-tools` | | Header | Required | Description | | ------------------------------------ | -------- | -------------------------------------------------------- | | `Authorization: Bearer ` | Yes | Your API key (`vnb-sk-*`) or JWT access token (`eyJ...`) | | `Content-Type: application/json` | Yes | Request body format | | `MCP-Protocol-Version` | Yes | Must be `2026-07-28` | | `Mcp-Method` | Yes | JSON-RPC method name (e.g., `tools/call`) | | `Mcp-Name` | Yes | Tool/resource/prompt name (e.g., `get_forecast`) | **Stateless MCP 2026-07-28** — No sessions, no initialize handshake, no `Mcp-Session-Id` header, no GET/DELETE endpoints. SSE streams are per-request on POST; close = cancel. ### Request body [#request-body] The body is a standard JSON-RPC 2.0 request. The `_meta` object in `params` may include `protocolVersion`, `clientInfo`, `clientCapabilities`. ```json { "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } } ``` ### Complete example (API key) [#complete-example-api-key] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_forecast" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } }' ``` ### Complete example (JWT access token) [#complete-example-jwt-access-token] ```bash TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_forecast" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } }' ``` *** ## MCP Methods [#mcp-methods] The apps you query speak the MCP protocol (version `2026-07-28`). Here are the methods you can call: ### server/discover (recommended first) [#serverdiscover-recommended-first] Discover the server's capabilities and supported versions: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: server/discover" \ -H "Mcp-Name: server/discover" \ -d '{"jsonrpc":"2.0","id":"1","method":"server/discover"}' ``` Response includes `supportedVersions`, `capabilities`, `serverInfo`, `instructions`. ### tools/list [#toolslist] Discover what tools an app provides: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/list" }' ``` Response includes `tools` array with `ttlMs` and `cacheScope` for spec-sanctioned caching. ### tools/call [#toolscall] Execute a tool with arguments: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_forecast" \ -d '{ "jsonrpc": "2.0", "id": "2", "method": "tools/call", "params": { "name": "get_forecast", "arguments": { "location": "London", "days": 3 } } }' ``` Response includes `resultType` (`complete` or `input_required` for MRTR). ### prompts/list [#promptslist] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: prompts/list" \ -H "Mcp-Name: prompts/list" \ -d '{"jsonrpc":"2.0","id":"3","method":"prompts/list"}' ``` ### prompts/get [#promptsget] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: prompts/get" \ -H "Mcp-Name: prompts/get" \ -d '{"jsonrpc":"2.0","id":"4","method":"prompts/get","params":{"name":"weather_summary","arguments":{"location":"London"}}}' ``` ### resources/list [#resourceslist] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: resources/list" \ -H "Mcp-Name: resources/list" \ -d '{"jsonrpc":"2.0","id":"5","method":"resources/list"}' ``` ### resources/read [#resourcesread] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: resources/read" \ -H "Mcp-Name: resources/read" \ -d '{"jsonrpc":"2.0","id":"6","method":"resources/read","params":{"uri":"weather://London/current"}}' ``` ### subscriptions/listen (optional) [#subscriptionslisten-optional] For servers that support change notifications: ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: subscriptions/listen" \ -H "Mcp-Name: subscriptions/listen" \ -d '{"jsonrpc":"2.0","id":"7","method":"subscriptions/listen","params":{"notifications":[]}}' ``` *** ## OAuth Endpoints [#oauth-endpoints] The Voidnet exposes the following OAuth 2.0 endpoints: | Endpoint | Description | | --------------------------------------------- | ------------------------------------------------------------------------------ | | `POST /oauth/token` | Issue access tokens (client\_credentials, authorization\_code, refresh\_token) | | `GET /authorize` | OAuth 2.1 authorization code + PKCE (interactive) | | `POST /authorize` | Process login+consent, issue authorization code | | `GET /.well-known/oauth-authorization-server` | RFC 8414 AS metadata | | `GET /.well-known/oauth-protected-resource` | RFC 9728 resource metadata | | `GET /.well-known/jwks.json` | Voidnet's public signing keys | ### AS metadata (RFC 8414) [#as-metadata-rfc-8414] ```json { "issuer": "https://api.openvoidnet.com", "authorization_endpoint": "https://api.openvoidnet.com/authorize", "token_endpoint": "https://api.openvoidnet.com/oauth/token", "jwks_uri": "https://api.openvoidnet.com/.well-known/jwks.json", "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"], "token_endpoint_auth_methods_supported": ["none", "client_secret_post"], "scopes_supported": ["mcp:tools", "llm:completions"], "response_types_supported": ["code"], "code_challenge_methods_supported": ["S256"] } ``` ### Protected resource metadata (RFC 9728) [#protected-resource-metadata-rfc-9728] ```json { "authorization_servers": ["https://api.openvoidnet.com"], "scopes_supported": ["mcp:tools", "llm:completions"], "bearer_methods_supported": ["authorization_header"] } ``` These endpoints let OAuth-aware clients configure themselves automatically without hardcoding token endpoints or signing keys. *** ## Error Handling [#error-handling] The Voidnet returns errors in one JSON shape. The `error` field is an **object**, not a string: ```json { "error": { "code": "error_code", "message": "Human-readable description", "status": 429 } } ``` For the complete catalog, see the [Error Reference](https://docs.openvoidnet.com/docs/reference/error-reference). *** ## Rate Limits & Metering [#rate-limits--metering] The Voidnet enforces a per-minute/per-day **rate limit** and a monthly **meter limit** per app. Exceeding either returns a `429`: * `rate_limit_exceeded` — too fast. Wait for the window to reset and retry with backoff. * `meter_limit_exceeded` — monthly quota exhausted. Wait for the rolling 30-day reset or upgrade your tier. * `meter_expired` — billing period ended. Wait for the meter to reset. There are no `X-RateLimit-*` response headers. Track your remaining quota in the Console under **Usage**. *** ## Calling Apps from Code [#calling-apps-from-code] ### TypeScript / JavaScript [#typescript--javascript] ```typescript const API_KEY = "vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"; const TOKEN_ENDPOINT = "https://api.openvoidnet.com/oauth/token"; const GATEWAY = "https://api.openvoidnet.com"; const USERNAME = "acmecorp"; const APPNAME = "weather-tools"; const COMMON_HEADERS = { "MCP-Protocol-Version": "2026-07-28", "Content-Type": "application/json" }; // Option A: API key (simple) async function callWithKey() { const response = await fetch(`${GATEWAY}/v1-beta/mcp/${USERNAME}/${APPNAME}`, { method: "POST", headers: { "Authorization": `Bearer ${API_KEY}`, ...COMMON_HEADERS, "Mcp-Method": "tools/call", "Mcp-Name": "get_forecast" }, body: JSON.stringify({ jsonrpc: "2.0", id: "1", method: "tools/call", params: { name: "get_forecast", arguments: { location: "London", days: 3 } } }) }); return await response.json(); } // Option B: Get an access token first (production) async function getAccessToken(): Promise { const resp = await fetch(TOKEN_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: API_KEY }) }); const data = await resp.json(); return data.access_token; } async function callWithToken() { const token = await getAccessToken(); const response = await fetch(`${GATEWAY}/v1-beta/mcp/${USERNAME}/${APPNAME}`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, ...COMMON_HEADERS, "Mcp-Method": "tools/list", "Mcp-Name": "tools/list" }, body: JSON.stringify({ jsonrpc: "2.0", id: "1", method: "tools/list" }) }); return await response.json(); } ``` ### Python [#python] ```python import requests API_KEY = "vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" GATEWAY = "https://api.openvoidnet.com" USERNAME = "acmecorp" APPNAME = "weather-tools" COMMON_HEADERS = { "MCP-Protocol-Version": "2026-07-28", "Content-Type": "application/json" } # Option A: API key (simple) response = requests.post( f"{GATEWAY}/v1-beta/mcp/{USERNAME}/{APPNAME}", headers={ "Authorization": f"Bearer {API_KEY}", **COMMON_HEADERS, "Mcp-Method": "tools/call", "Mcp-Name": "get_forecast" }, json={ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_forecast", "arguments": {"location": "London", "days": 3} } } ) data = response.json() print(data["result"]) # Option B: Get an access token first (production) token_resp = requests.post( "https://api.openvoidnet.com/oauth/token", data={ "grant_type": "client_credentials", "client_id": API_KEY } ) token = token_resp.json()["access_token"] response = requests.post( f"{GATEWAY}/v1-beta/mcp/{USERNAME}/{APPNAME}", headers={ "Authorization": f"Bearer {token}", **COMMON_HEADERS, "Mcp-Method": "tools/list", "Mcp-Name": "tools/list" }, json={"jsonrpc": "2.0", "id": "1", "method": "tools/list"} ) ``` ### cURL (reusable) [#curl-reusable] ```bash GATEWAY="https://api.openvoidnet.com" API_KEY="vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" USERNAME="acmecorp" APPNAME="weather-tools" # Authenticate with API key directly curl -X POST "$GATEWAY/v1-beta/mcp/$USERNAME/$APPNAME" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/list" }' # Or exchange for a JWT access token first TOKEN=$(curl -s -X POST "$GATEWAY/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=$API_KEY" | jq -r '.access_token') curl -X POST "$GATEWAY/v1-beta/mcp/$USERNAME/$APPNAME" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}' ``` *** ## FAQ [#faq] ### Do I need to purchase an app before calling it? [#do-i-need-to-purchase-an-app-before-calling-it] Yes — even free tiers require a purchase record. Click "Get" in the marketplace to activate a free app. This creates the necessary record for the Voidnet to authorize your requests. ### How do I find which apps I've purchased? [#how-do-i-find-which-apps-ive-purchased] Go to the **Usage** page in your Console dashboard under the **Buyer** section. You'll see all apps you've purchased, their tier status, API call counts, and remaining meter usage. ### Can I use the same API key for multiple apps? [#can-i-use-the-same-api-key-for-multiple-apps] Yes — a single API key works across all apps you purchase. ### How do I know which methods an app supports? [#how-do-i-know-which-methods-an-app-supports] Call `server/discover` (recommended first) or `tools/list`, `prompts/list`, `resources/list` to discover capabilities. ### When should I use a JWT access token instead of my API key? [#when-should-i-use-a-jwt-access-token-instead-of-my-api-key] Use JWT access tokens in production systems, CI/CD pipelines, and shared environments. They're short-lived (default 1 hour), reducing the risk of credential exposure. Use raw API keys for local development and testing. ### Can I use both API keys and JWT tokens? [#can-i-use-both-api-keys-and-jwt-tokens] Yes — the Voidnet accepts both. OAuth access tokens (`eyJ...`) and API keys (`vnb-sk-*`) are both valid in the `Authorization: Bearer` header. The Voidnet detects the format automatically. ### What happens when my access token expires? [#what-happens-when-my-access-token-expires] The Voidnet returns `api_key_expired` (401). Request a new token from `POST /oauth/token` using your API key (`client_credentials`) or `refresh_token`, then retry. ### Can my organization use Okta or Entra ID to authenticate? [#can-my-organization-use-okta-or-entra-id-to-authenticate] Not yet. The Voidnet authorization server issues tokens via `client_credentials` (exchange API key for JWT) and `authorization_code` + PKCE (interactive). Enterprise identity federation (jwt-bearer) is not currently supported. # MCP OAuth (Preview) (/docs/mcp/oauth) > **In preview** — OAuth login flows are still stabilizing. API keys work everywhere OAuth does; prefer keys unless you need short-lived scoped tokens. The Voidnet acts as an OAuth 2.0 authorization server, issuing JWT access tokens for production systems. ## Token types [#token-types] ### JWT access tokens [#jwt-access-tokens] Access tokens are JSON Web Tokens (JWTs) signed with RS256. They are three base64url-encoded segments separated by dots: ``` eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature ``` Each token contains: * **`sub`** — Buyer UUID * **`scope`** — Permission scope (`mcp:tools` for tool calls, `llm:completions` for chat completions) * **`exp`** — Expiration time (default 1 hour) * **`iat`** — Issued at time * **`iss`** — Voidnet issuer URL * **`aud`** — Voidnet issuer URL (fixed at mint time; no client-controlled audience input) * **`kid`** — Key ID identifying the signing key in the JWKS ### API keys [#api-keys] The Voidnet also accepts API keys (`vnb-sk-*`) as a simpler alternative. See the [Buyer Guide](https://docs.openvoidnet.com/docs/mcp/buyer) for API key management. ## Grant types [#grant-types] ### client\_credentials [#client_credentials] Exchange your API key for a short-lived JWT: ```bash curl -X POST http://localhost:8090/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" ``` Successful response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600, "scope": "mcp:tools" } ``` The returned `access_token` is a JWT usable in any `Authorization: Bearer` header. **Notes:** * No separate `client_secret` is needed — the API key itself is the secret * An optional `scope` parameter is accepted (`mcp:tools` default, `llm:completions` for chat) * The `aud` claim is always the Voidnet issuer URL — the token endpoint accepts no client-controlled audience input ### authorization\_code (+ PKCE, interactive clients) [#authorization_code--pkce-interactive-clients] For OAuth-aware MCP clients (Claude Desktop, Cursor, VS Code). The gateway hosts the login+consent page — no redirect to any accounts app: 1. Open `GET /authorize?client_id=&redirect_uri=&response_type=code&code_challenge=<43-char-base64url>&code_challenge_method=S256&scope=mcp:tools&state=` — renders the login+consent form. 2. User submits email+password via `POST /authorize` — issues a single-use authorization code (5-minute TTL) and redirects to `redirect_uri?code=...&state=...`. 3. Exchange the code at `POST /oauth/token` (`grant_type=authorization_code`, plus `code`, `redirect_uri`, `code_verifier`) — returns access token **and** refresh token. PKCE `S256` is mandatory; `redirect_uri` must be an exact `http(s)` match. ### refresh\_token [#refresh_token] Refresh tokens live 30 days and rotate on every use (single-use — reuse is rejected): ```bash curl -X POST http://localhost:8090/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" \ -d "refresh_token=" ``` The response contains a new access token **and** a new refresh token — store both, discard the old refresh token. ## Using tokens [#using-tokens] All requests to the Voidnet use the `Authorization: Bearer` header: ```bash # API key (local gateway on 8090, prod on 8080 / api.openvoidnet.com) curl -X POST http://localhost:8090/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{"jsonrpc": "2.0", "id": "1", "method": "tools/list"}' # JWT access token TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." curl -X POST http://localhost:8090/v1-beta/mcp/acmecorp/weather-tools \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{"jsonrpc": "2.0", "id": "1", "method": "tools/list"}' ``` Every billable routed call carries `X-Voidnet-Request-Id` downstream to the publisher and upstream to the buyer. Match that value to console Usage Logs `requestId` to reconcile call by call. Non-billable discovery and list calls, including cache-served lists, carry no metering row. The Voidnet auto-detects the credential type — three dot-separated segments means JWT, otherwise API key. ### Token expiration [#token-expiration] Tokens have an `exp` claim. When a token expires, the Voidnet returns `api_key_expired` (401): 1. Detect the `401` response 2. Request a new token from `POST /oauth/token` 3. Retry the request with the new token ## Publisher authentication [#publisher-authentication] When the Voidnet proxies a request to a publisher's server, it authenticates using the publisher's API key (`vnp-sk-*`): ``` Authorization: Bearer vnp-sk-27fc6268dc64a9ba2c4cb92489e9175cbf404e260beb268b976f1a56582eff3 ``` Publisher keys are: * Generated in the Console when publishing an app * The Voidnet always sends `vnp-sk-*` regardless of how the buyer authenticated * Sent to the publisher server on every proxied request ## OAuth endpoints [#oauth-endpoints] The Voidnet acts as an OAuth 2.0 authorization server. Full interactive reference (parameters, responses, Try-it) lives on the generated [MCP API page](https://docs.openvoidnet.com/docs/reference/api/mcp): | Endpoint | Description | | --------------------------------------------- | --------------------------------------------------------------------------------- | | `POST /oauth/token` | Issue access tokens (`client_credentials`, `authorization_code`, `refresh_token`) | | `GET /authorize` | Render the login+consent form (interactive clients) | | `POST /authorize` | Process login+consent, issue authorization code, redirect | | `GET /.well-known/oauth-authorization-server` | RFC 8414 AS metadata | | `GET /.well-known/oauth-protected-resource` | RFC 9728 resource metadata | | `GET /.well-known/jwks.json` | Voidnet's public signing keys | ## Credential management [#credential-management] **API keys** are managed through the Voidnet: * **Generate** — Create a new key from the API Keys section * **Revoke** — Immediately invalidate a compromised key * **Rotate** — Generate a new key, update your configuration, revoke the old one **OAuth access tokens** are obtained programmatically: * **client\_credentials** — Exchange an API key for a JWT at `POST /oauth/token` * Tokens expire after a configurable period (default 1 hour) ## Credential security best practices [#credential-security-best-practices] * Store API keys and tokens in environment variables, not in code * Use a secrets manager in production * Rotate API keys periodically * Revoke compromised keys immediately * Never expose credentials in client-side code, logs, or version control * Use short-lived JWT access tokens in production systems instead of raw API keys # MCP Overview (/docs/mcp/overview) MCP apps expose callable **tools**, URI-addressable **resources**, and reusable **prompts** through one gateway endpoint. Protocol version `2026-07-28`, stateless: no sessions, no initialize handshake, no `Mcp-Session-Id`, no GET/DELETE endpoints. SSE streams are per-request on POST; close = cancel. ## At a glance [#at-a-glance] | | | | ---------------- | --------------------------------------------------------------------------------------------- | | Endpoint | `POST /v1-beta/mcp/{username}/{appname}` | | Required headers | `Authorization`, `Content-Type`, `MCP-Protocol-Version: 2026-07-28`, `Mcp-Method`, `Mcp-Name` | | Billing unit | Requests (per-call on usage-based pricing) | | Start here | [Buyer Guide](https://docs.openvoidnet.com/docs/mcp/buyer) to call · [Publisher Guide](https://docs.openvoidnet.com/docs/mcp/publisher) to publish | ## Methods [#methods] `server/discover` (capabilities first), `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`, `subscriptions/listen` (optional). Full shapes with examples live in the [Buyer Guide](https://docs.openvoidnet.com/docs/mcp/buyer). ## Rules that bite [#rules-that-bite] * `MCP-Protocol-Version` must equal `_meta.protocolVersion` in the body, or the call fails `400 HeaderMismatch`. * Every proxied request carries your publisher key (`vnp-sk-*`); validate it on every request. * Billable `tools/call` responses meter one request; list/discover calls never bill. # MCP Publisher Guide (/docs/mcp/publisher) This guide covers everything you need to build, deploy, and publish an MCP server on Voidnet Console — from server implementation to Stripe payout configuration. *** ## Overview [#overview] A **publisher** is a developer who builds and hosts an MCP server and makes it available to buyers through the Voidnet Console marketplace. The Voidnet proxies buyer requests to your server, enforces rate limits and metering, and handles billing. Your server only needs to implement the standard MCP protocol and validate the publisher API key. ``` Your Apps / AI (Key or OAuth Token) → Voidnet → Your MCP Server ↑ Publisher API Key (always vnp-sk-*) ``` **Buyers authenticate to the Voidnet** using either an API key (`vnb-sk-*`) or an OAuth 2.0 access token (JWT). The Voidnet validates their identity, then proxies the request to your server with your publisher API key (`vnp-sk-*`) in the `Authorization` header. Your server never sees the buyer's credentials — only the publisher key you already trust. *** ## Protocol position [#protocol-position] The Voidnet speaks stateless MCP `2026-07-28` only: `server/discover` for capabilities, no `initialize` handshake, no sessions. This contract is fixed at the gateway and does not negotiate. If your server is built on the standard SDK, do not rewrite it. Wrap it with the Voidnet SDK — one call that speaks the stateless dialect on the Voidnet wire while your existing logic keeps serving its current clients unchanged. See the [Voidnet CLI guide](https://docs.openvoidnet.com/docs/cli/overview) for the adapter and the machine API. Translation lives in the kit we ship and own; the gateway stays strict so every routed call keeps identical metering, auth, and billing semantics. *** ## Publisher API Key [#publisher-api-key] Every published app gets a publisher API key. This key is generated in the [Voidnet Console](https://openvoidnet.com/console) when you publish an app and is **shown only once** — save it securely. ``` vnp-sk-27fc6268dc64a9ba2c4cb92489e9175c9bf404e260beb268b976f1a56582eff3 ``` ### How it's used [#how-its-used] When the Voidnet proxies a request to your server, it sends the key in the `Authorization` header: ``` Authorization: Bearer vnp-sk-27fc6268... ``` **Your server must validate this header** on every request and reject invalid or missing keys with a `401` response. ### Key lifecycle [#key-lifecycle] * **Generate** — Created when you publish an app or manually from the Console * **Revoke** — You can revoke keys at any time; revoked keys immediately stop working * **Rotate** — Generate a new key, update your server, then revoke the old one * **Storage** — Keys are encrypted at rest in the Voidnet database *** ## Server Requirements [#server-requirements] Your MCP server must meet these requirements to work with Voidnet Console: ### 1. HTTPS [#1-https] All communication with the Voidnet is over HTTPS. You'll need a valid TLS certificate. ### 2. Single MCP endpoint [#2-single-mcp-endpoint] The Voidnet sends all MCP requests to a single endpoint on your server — typically `/mcp`. This single endpoint handles all MCP methods. ### 3. Protocol version [#3-protocol-version] Voidnet Console requires **MCP protocol version `2026-07-28`** (stateless). Your server should: * Accept the `MCP-Protocol-Version: 2026-07-28` header * Include `2026-07-28` in the `supportedVersions` array of the `server/discover` response * Reject unsupported versions with `400` and error code `-32022` **No sessions, no initialize handshake, no GET/DELETE endpoints.** The legacy 2025-11-25 stateful model is not supported. ### 4. Authentication [#4-authentication] Your server must validate the `Authorization: Bearer ` header and return `401` for invalid or missing keys. See [Authentication](#authentication) below. ### 5. Required headers [#5-required-headers] The Voidnet forwards these headers on every request: | Header | Description | | ---------------------- | ----------------------------------------------------------------------- | | `MCP-Protocol-Version` | `2026-07-28` (gateway's protocol version) | | `Mcp-Method` | JSON-RPC method name (e.g., `tools/call`) | | `Mcp-Name` | Tool/resource/prompt name for routing | | `Authorization` | `Bearer vnp-sk-...` (your publisher API key) | | `X-Voidnet-Request-Id` | Join ID on every billable call — log it to reconcile against Usage Logs | Your server must **validate** that `MCP-Protocol-Version` matches the `_meta.protocolVersion` in the JSON-RPC request body. Mismatch returns `400` with error code `-32020` (HeaderMismatch). *** ## Authentication [#authentication] The authentication contract between the Voidnet and your server is: ``` Request: Authorization: Bearer vnp-sk-27fc6268... Response (valid key): 200 OK Response (invalid or missing key): 401 Unauthorized { "jsonrpc": "2.0", "error": { "code": -32001, "message": "Unauthorized", "data": "Invalid or missing publisher API key" } } ``` Validate on **every request**. If the key is invalid, return `401` immediately — do not process the MCP request. *** ## MCP Protocol (stateless 2026-07-28) [#mcp-protocol-stateless-2026-07-28] Your server must implement the following MCP 2026-07-28 methods. **No initialize handshake, no sessions, no `MCP-Session-Id` headers.** ### server/discover (REQUIRED) [#serverdiscover-required] The Voidnet calls `server/discover` to discover your server's capabilities. This replaces the legacy `initialize` handshake. **Request:** ```json { "jsonrpc": "2.0", "id": 1, "method": "server/discover" } ``` **Response:** ```json { "jsonrpc": "2.0", "id": 1, "result": { "supportedVersions": ["2026-07-28"], "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": false, "listChanged": true }, "prompts": { "listChanged": true } }, "serverInfo": { "name": "my-server", "version": "1.0.0" }, "instructions": "Optional usage instructions for your server" } } ``` * `supportedVersions`: Must include `2026-07-28` * `capabilities`: Your server's capabilities (tools, resources, prompts) * `serverInfo`: Your server's name and version * `instructions`: Optional human-readable instructions ### tools/list [#toolslist] Returns the tools your server provides: ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } ``` **Response** (include `ttlMs` and `cacheScope` for spec-sanctioned caching): ```json { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "my_tool", "description": "Does something useful", "inputSchema": { "type": "object", "properties": { "input": { "type": "string", "description": "The input value" } }, "required": ["input"] } } ], "ttlMs": 300000, "cacheScope": "private" } } ``` ### tools/call [#toolscall] Invokes a tool with the provided arguments: ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "my_tool", "arguments": { "input": "hello" } } } ``` **Response** (include `resultType` — `complete` or `input_required` for MRTR): ```json { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "Result: hello" } ], "resultType": "complete" } } ``` ### resources/list (optional) [#resourceslist-optional] ```json { "jsonrpc": "2.0", "id": 4, "method": "resources/list" } ``` **Response:** ```json { "jsonrpc": "2.0", "id": 4, "result": { "resources": [ { "uri": "data://my-resource", "name": "My Resource", "description": "A data resource", "mimeType": "text/plain" } ], "ttlMs": 300000, "cacheScope": "private" } } ``` ### resources/read (optional) [#resourcesread-optional] ```json { "jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": { "uri": "data://my-resource" } } ``` **Response:** ```json { "jsonrpc": "2.0", "id": 5, "result": { "uri": "data://my-resource", "mimeType": "text/plain", "text": "Resource content here", "ttlMs": 300000, "cacheScope": "private" } } ``` ### prompts/list (optional) [#promptslist-optional] ```json { "jsonrpc": "2.0", "id": 6, "method": "prompts/list" } ``` ### prompts/get (optional) [#promptsget-optional] ```json { "jsonrpc": "2.0", "id": 7, "method": "prompts/get", "params": { "name": "system_instruction", "arguments": { "task": "write a summary" } } } ``` ### subscriptions/listen (optional) [#subscriptionslisten-optional] For servers that support change notifications. The Voidnet calls this once per subscription: ```json { "jsonrpc": "2.0", "id": 8, "method": "subscriptions/listen", "params": { "notifications": [] } } ``` Acknowledge with `notifications/subscriptions/acknowledged`. *** ## Server Verification [#server-verification] When you publish an app in the Console, Voidnet performs a **live verification** of your server: 1. The Voidnet calls `server/discover` to discover your server's capabilities 2. It calls `tools/list`, `resources/list`, and `prompts/list` 3. The discovered capabilities are cached and displayed in the Console Your server must implement `server/discover` and the list methods above. Verification determines which tools, resources, and prompts buyers will see. You can re-verify your server at any time from the app detail page — click **Refresh** in the MCP Configuration section. *** ## Domain Verification [#domain-verification] Before you can publish an app, you must verify ownership of your server's domain: 1. **Initiate** — In the Console, start domain verification. A unique verification token is generated. 2. **Place the file** — Host the verification file at your domain's root or `/.well-known/` directory: ``` https://your-domain.com/voidnet-site-verification-.html ``` 3. **Verify** — Voidnet Console fetches the file and confirms the content matches the expected token. 4. **Done** — The domain is marked as verified and can be used for publishing. One verified domain can host multiple apps. *** ## Tier Configuration [#tier-configuration] Every published app has configurable tiers: ### Free Tier [#free-tier] * **Meter limit** — Max requests per month before the free tier is exhausted * **Rate limit per minute** — Max requests per minute * **Rate limit per day** — Max requests per day ### Paid Tier [#paid-tier] * Same limits as free tier but higher * **Price** — Set in USD with monthly, yearly, or one-time billing * Requires a connected Stripe account (see below) ### How tiers work [#how-tiers-work] 1. A buyer discovers your app in the marketplace 2. If you offer a free tier, the buyer can start using it immediately 3. If you offer a paid tier, the buyer purchases a subscription 4. The Voidnet enforces rate limits and metering based on the buyer's tier 5. When a free tier buyer exhausts their meter, they're prompted to upgrade *** ## Stripe Connect Billing [#stripe-connect-billing] To accept payments, you need a Stripe Express account connected to Voidnet Console: 1. **Connect** — In the Console Payments page, click "Connect with Stripe" 2. **Onboard** — Complete Stripe Express onboarding (identity verification, bank account) 3. **Sync** — Your Stripe account status appears in the Console ### Payout structure [#payout-structure] | Component | Share | | ---------------------------- | ------------- | | Publisher payout | 80% (default) | | Voidnet Console platform fee | 20% (default) | Per-app — the default 80/20 can be lowered on individual apps (negotiated deals, set by Voidnet on request). The rate is locked at publish time; each purchase splits from that locked rate. Payouts are sent directly by Stripe to your connected bank account on Stripe's standard payout schedule. ### Pricing model [#pricing-model] * The Voidnet uses the **Marketplace / Direct Charges** model * You are the merchant of record for each transaction * The Voidnet creates a PaymentIntent with an `application_fee` for Voidnet Console's per-app fee (default 20%) * You receive 80% (default) — rounded: `fee = floor(amount*percent/100)`, `payout = amount - fee` so the two always sum to gross exactly (platform absorbs the cent). Stripe processing fees are separate and deducted from the gross before the split. *** ## Testing Your Server [#testing-your-server] ### 1. Local development [#1-local-development] Run your MCP server locally with HTTPS (self-signed certs are fine for testing). The Console's verify-server tool supports localhost with `rejectUnauthorized: false` for external publishers. Internally, Voidnet engineers use `http` on localhost — external publishers must use `https`. ### 2. Verify server in Console [#2-verify-server-in-console] From the **Publish MCP Interface** page: 1. Enter your **App Name** (unique per publisher) 2. Select a **verified domain** — the server URL is auto-populated from your domain 3. Add a **description** 4. Click **Verify Server** 5. The Console calls `server/discover` → `tools/list` → `resources/list` → `prompts/list` and displays your capabilities 6. **Save your publisher API key** — shown once in the success dialog 7. Configure tiers and click **Submit** to publish ### 3. Test with curl [#3-test-with-curl] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/your-username/your-app \ -H "Authorization: Bearer vnb-sk-your-buyer-key" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' ``` *** ## Going Live [#going-live] ### Prerequisites checklist [#prerequisites-checklist] * [ ] Server is running HTTPS with a valid certificate * [ ] Server validates the `Authorization: Bearer ` header * [ ] Server implements MCP `2026-07-28` protocol (stateless) * [ ] Server implements `server/discover` (required) * [ ] Server exposes at least one tool * [ ] Domain is verified * [ ] Stripe Express account is connected (if offering paid tier) ### Publishing flow [#publishing-flow] 1. Go to **Console → Publish Apps** and select **AI Tools (MCP)** 2. Enter your **App Name** (unique per publisher, like a GitHub repo name) 3. Select a **verified domain** to set the server URL 4. Add a **description** 5. Click **Verify Server** — the Voidnet probes your MCP server with `server/discover` and discovers its capabilities 6. Review the detected **tools, resources, and prompts** 7. **Save your publisher API key** (`vnp-sk-*`) — it's shown only once in the success dialog. Without it, the Voidnet cannot proxy requests to your server 8. Close the dialog and configure **tiers** — free, paid, or both, with rate limits and metering limits 9. Click **Submit** — your app is created as a draft and immediately published to the marketplace 10. Your app now appears in **Published Apps** and the [Voidnet Console Marketplace](https://openvoidnet.com/marketplace) ### Drafts [#drafts] Every app starts as a **draft** when you first verify the server. Drafts appear in the **Publish Apps** page under "Continue your drafts" and can be resumed at any time. Publishing (Submitting) sets the status to `published`, making the app visible in the marketplace. | Status | Visible in marketplace | Modifiable | | ----------- | ---------------------- | ------------------------------------------ | | `draft` | No | Yes — can edit tiers, re-verify, or delete | | `published` | Yes | Yes — can update, re-verify, or delete | ### Managing your app [#managing-your-app] * **View details** — Console → Published Apps → click your app * **API keys** — Generate and revoke keys from the API Keys tab on the app detail page * **Logo** — Upload or remove an app logo from the app detail page (400×400 WebP, PNG, JPEG, or AVIF) * **Re-verify** — Refresh server capabilities from the Overview tab on the app detail page * **Analytics** — Track requests and revenue per app * **Delete** — Remove your app (irreversible) from either the Published Apps list or Publish Apps page *** ## Reference: Server skeleton [#reference-server-skeleton] A minimal MCP server in TypeScript/Node.js (stateless 2026-07-28): ```typescript import { createServer } from "https"; import { readFileSync } from "fs"; const PUBLISHER_API_KEY = process.env.PUBLISHER_API_KEY; const PORT = Number(process.env.PORT ?? 3051); // Read and parse the JSON-RPC request body (1 MB cap). function parseBody(req: import("http").IncomingMessage): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; let size = 0; req.on("data", (chunk: Buffer) => { size += chunk.length; if (size > 1024 * 1024) { reject(new Error("Request body too large")); req.destroy(); return; } chunks.push(chunk); }); req.on("end", () => { try { resolve(JSON.parse(Buffer.concat(chunks).toString("utf8"))); } catch (err) { reject(err); } }); req.on("error", reject); }); } const server = createServer( { key: readFileSync("./key.pem"), cert: readFileSync("./cert.pem") }, async (req, res) => { if (req.method === "GET" || req.method === "DELETE") { res.writeHead(405, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32601, message: "Method not allowed" } })); return; } // 1. Validate publisher API key const expected = `Bearer ${PUBLISHER_API_KEY}`; if (PUBLISHER_API_KEY && req.headers["authorization"] !== expected) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" } })); return; } // 2. Validate MCP-Protocol-Version header const protocolVersion = req.headers["mcp-protocol-version"] as string | undefined; if (protocolVersion !== "2026-07-28") { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32022, message: "UnsupportedProtocolVersion", data: { supported: ["2026-07-28"], requested: protocolVersion } } })); return; } // 3. Parse MCP JSON-RPC and validate _meta protocolVersion matches header const body = await parseBody(req); const metaVersion = body.params?._meta?.protocolVersion ?? body.params?._meta?.["io.modelcontextprotocol/protocolVersion"]; if (metaVersion && metaVersion !== protocolVersion) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32020, message: "HeaderMismatch" } })); return; } // 4. Dispatch in your own handleMcpRequest function — handle these for verification and store: // server/discover, tools/list, tools/call, resources/list, resources/read, // resources/templates/list, prompts/list, prompts/get, subscriptions/listen // and return the JSON-RPC result object with ttlMs/cacheScope + resultType: "complete". const response = await handleMcpRequest(body); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(response)); }); server.listen(PORT, () => console.log(`MCP listening on :${PORT}`)); ``` Direct publisher test (no gateway): ```bash curl -X POST http://localhost:3051/mcp \ -H "Authorization: Bearer vnp-sk-test..." \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: server/discover" -H "Mcp-Name: server/discover" \ -d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}' ``` > **Coming next:** `@openvoidnet/mcp-sdk` will wrap `McpServer` + `HttpTransport` so you don't copy this skeleton. See the [Voidnet test server](https://github.com/void-labs/mcp-test-server-https) for a complete reference implementation. # Billing (/docs/money/billing) How money moves on Voidnet Console: buyers prepay wallet credits, usage deducts from the wallet, publishers earn on every deduct and get paid weekly. ## Wallet [#wallet] The wallet is prepaid balance held in milli-cents (1 cent = 1,000 milli). It is the single source of truth for all paid usage. * **Top up** at **Console → Wallet**: $5–$1000 per top-up via Stripe Checkout. You return to the wallet page and the balance credits automatically. Every top-up is idempotent — retries and double callbacks never credit twice. * **Spend** happens automatically: paid calls and fixed-price grants deduct from the balance. An empty wallet returns `402 insufficient_balance` instead of calling the publisher. * **Track** balance and the last 50 transactions (top-ups, deducts) in **Console → Wallet**. Total spending per app lives in **Console → Usage**. ## Pricing models [#pricing-models] Publishers price paid tiers one of three ways, shown on every Marketplace app page: | Interval | Deducted | Example | | ------------- | ---------------------------------------------------------------------- | ------------- | | `per_million` | Per call: `tokens / 1M × price` (LLM) or `requests / 1M × price` (MCP) | $10/1M tokens | | `month` | Full price once at purchase | $9.99/month | | `one_time` | Full price once at purchase | $49 once | Free tiers move no money. Buyers on a free tier upgrade to paid from the app page; the free purchase is revoked when the paid grant completes. One active tier per buyer per app. ## Platform fee [#platform-fee] Every paid deduct splits the same way: `fee = floor(gross × pct / 100)`, `payout = gross − fee`, so both sides always sum to gross exactly. The default split is **80% publisher / 20% platform**: the rate is locked per app at publish time, and each purchase splits from that locked rate and is recorded in the Publisher Agreement. Negotiated rates are set by Voidnet on request. ## Publisher payouts [#publisher-payouts] Wallet earnings accumulate in the platform ledger and pay out **weekly on Fridays**: one Stripe Transfer per publisher per week covering all apps and sales, after a 7-day settlement hold. Each payout records week, sales count, gross, fee, payout, transfer id, and status, with per-app breakdowns — see it all in **Console → Payments → Weekly Payouts**. Legacy Stripe-checkout earnings split automatically at charge time (destination charges) and never enter this ledger. There are no buyer-facing refunds. ## Stripe Connect [#stripe-connect] Publishers connect a Stripe account in **Console → Payments**. **Every paid tier requires an active `stripe_transfers` capability** — publishing a paid tier without it is rejected, because earnings would have no payout destination. Keep onboarding complete and details current or weekly transfers fail loudly instead of queuing silently. # Usage and Reconciliation (/docs/money/usage) Every call made through Voidnet appears in your dashboard so you can count it yourself and compare. ## How counting works [#how-counting-works] A request is counted when a buyer successfully calls a billable tool on your server. The Console calls this a request. List and discover calls are not counted. If the tool returns an error, it is still shown in the log so you can see what happened, but it is not billed. Each counted request records a request ID, the time it was received, the tool that was called, how long your server took to respond, and whether the buyer was on a free or paid tier. ## Where to see it [#where-to-see-it] Open **Console → Published Apps → your app → Usage Logs**. The table lists every counted request for that app, most recent first. You can copy the request ID and match it to the ID in your own server logs. The time column uses the gateway's clock — compare it to your server's clock with a small window for clock drift. Use the pagination controls to page through older calls and **Export CSV** to download the current page for offline comparison. ## Reconciling with your own logs [#reconciling-with-your-own-logs] Your server logs the same request ID on every call forwarded from Voidnet. To reconcile: 1. Log the request ID and time on your server for each tool call. 2. Open the Usage Logs tab for that app. 3. Filter or page to the same time window. 4. Compare counts. Every counted call appears in the log. There are no hidden states — if a call is counted, it is listed. If it is not listed, it was not counted. If counts differ, check for calls that returned an error or were filtered as non-billable in the docs. If you still see a mismatch, contact support with the request IDs and times. ## API access [#api-access] The same data is available via a publisher API for programmatic reconciliation: ``` GET /api/console/publishers/apps/{appId}/metering?limit=50&offset=0&from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z ``` The API requires a console session. It returns `records` with `requestId`, `timestamp`, `appName`, `toolName`, `operationType`, `tier`, `requests`, `durationMs`, `error`, and `buyerId`, plus `pagination` with `total`. ## Notes [#notes] Counts are final once they appear in the log. The `v1-beta` endpoint family is versioned, and any future change to counting will be announced in the changelog with a deprecation window. # A2A Publisher Guide (/docs/publisher-guide/a2a) > **Coming soon** — A2A (Agent-to-Agent) protocol support is in development. This guide will cover how to build, deploy, and publish an A2A agent on Voidnet Console. ## What to expect [#what-to-expect] ### Overview [#overview] * What A2A agents are and the agent-to-agent paradigm * How the Voidnet proxies agent-to-agent communication * A2A protocol version `1.0` specification ### Server requirements [#server-requirements] * HTTPS endpoint requirements * A2A protocol compliance * Authentication with publisher API keys (`vnp-sk-*`) ### Publishing flow [#publishing-flow] * Registering an A2A agent in the Console * Live verification of agent capabilities * Domain verification requirements * Tier configuration and pricing ### Agent capabilities [#agent-capabilities] * Defining agent skills and task types * Agent card structure * Streaming and polling support ### Protocol reference [#protocol-reference] * Agent-to-agent message formats * Task lifecycle management * Error handling and retries *** Check back closer to launch for the full guide. For now, see the [MCP Publisher Guide](https://docs.openvoidnet.com/docs/publisher-guide/mcp) to start publishing tools on Voidnet Console today. # LLM Publisher Guide (/docs/publisher-guide/llm) Publish an AI model as a Void App. One model per app, billed per token. Your server just needs to speak OpenAI-compatible `POST /v1/chat/completions`. ## Quick start [#quick-start] Use any OpenAI-compatible server — vLLM, Ollama, or similar: ```bash # Ollama ollama pull smollm2:135m curl http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" \ -d '{"model":"smollm2:135m","messages":[{"role":"user","content":"hi"}],"stream":false}' # vLLM vllm serve HuggingFaceTB/SmolLM2-135M-Instruct --host 0.0.0.0 --port 9000 --served-model-name smollm2:135m curl http://localhost:9000/v1/chat/completions -H "Content-Type: application/json" \ -d '{"model":"smollm2:135m","messages":[{"role":"user","content":"hi"}]}' ``` Both return: ```json {"choices":[{"message":{"content":"Hello"}}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}} ``` ## Publish (Console) [#publish-console] 1. Verify a domain: **Console → Domains** → follow the verification steps. 2. Go to **Publish Apps → LLM** and fill in: * **App Name** — unique per publisher `a-z, 0-9, -, _` (3–50 chars) * **Model** — the exact model ID your server serves (for example `HuggingFaceTB/SmolLM-135M-Instruct`) * **Server URL** — `https://your-host/v1` (must end with `/v1`; the gateway will call `/chat/completions` on it) 3. Click **Create Draft** — you will receive a publisher API key. The gateway uses it to authenticate to your server, so your server should accept any `Authorization: Bearer ` header. 4. In the overview click **Publish** to make your app live in the Marketplace. You can configure free and paid tiers with token limits and pricing during publishing. ## Your server contract [#your-server-contract] Your server must expose a single endpoint: **`POST {serverUrl}/v1/chat/completions`** **Request — the gateway forwards the buyer's JSON verbatim:** ```json {"model":"your-model-id","messages":[{"role":"user","content":"hi"}],"temperature":0.7,"max_tokens":64,"stream":false} ``` `model` and `messages` are required. Multimodal content (`image_url`, `audio`, `video`) is not supported in V1 and will be rejected before reaching your server. **Response — you must return valid OpenAI shape:** ```json { "id": "chatcmpl-...", "object": "chat.completion", "created": 123, "model": "your-model-id", "choices": [{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}], "usage": {"prompt_tokens":10,"completion_tokens":5,"total_tokens":15} } ``` `total_tokens` must be present and greater than zero. Return `4xx`/`5xx` with `{"error":{"message":"..."}}` on failure. **Auth:** Accept any `Authorization: Bearer ` header. **Limits:** Response must be under 16MB. ## Billing [#billing] Apps are metered per token (`total_tokens` from your response). Your free and paid tier limits are token limits. The platform fee is 20%. ## Test via gateway [#test-via-gateway] After your app is published and a buyer has purchased it (free or paid): ```bash curl -X POST "https://api.openvoidnet.com/v1-beta/llm/{publisher}/{app}" \ -H "Authorization: Bearer vnb-sk-..." \ -H "Content-Type: application/json" \ -d '{"model":"your-model-id","messages":[{"role":"user","content":"hello"}]}' ``` Or with the OpenAI SDK — just change the `base_url`: ```python from openai import OpenAI client = OpenAI(base_url="https://api.openvoidnet.com/v1-beta/llm//", api_key="vnb-sk-...") response = client.chat.completions.create(model="your-model-id", messages=[{"role":"user","content":"hi"}]) print(response.choices[0].message.content, response.usage.total_tokens) ``` ## Limits V1 [#limits-v1] TEXT only, single model per app, no embeddings. `stream:true` is supported (SSE) — your stream must end with a final usage chunk. Expose `GET /v1/models` so buyers can list your model. See [Buyer Guide (LLM)](https://docs.openvoidnet.com/docs/buyer-guide/llm). # MCP Publisher Guide (/docs/publisher-guide/mcp) This guide covers everything you need to build, deploy, and publish an MCP server on Voidnet Console — from server implementation to Stripe payout configuration. *** ## Overview [#overview] A **publisher** is a developer who builds and hosts an MCP server and makes it available to buyers through the Voidnet Console marketplace. The Voidnet proxies buyer requests to your server, enforces rate limits and metering, and handles billing. Your server only needs to implement the standard MCP protocol and validate the publisher API key. ``` Your Apps / AI (Key or OAuth Token) → Voidnet → Your MCP Server ↑ Publisher API Key (always vnp-sk-*) ``` **Buyers authenticate to the Voidnet** using either an API key (`vnb-sk-*`) or an OAuth 2.0 access token (JWT). The Voidnet validates their identity, then proxies the request to your server with your publisher API key (`vnp-sk-*`) in the `Authorization` header. Your server never sees the buyer's credentials — only the publisher key you already trust. *** ## Publisher API Key [#publisher-api-key] Every published app gets a publisher API key. This key is generated in the [Voidnet Console](https://openvoidnet.com/console) when you publish an app and is **shown only once** — save it securely. ``` vnp-sk-27fc6268dc64a9ba2c4cb92489e9175c9bf404e260beb268b976f1a56582eff3 ``` ### How it's used [#how-its-used] When the Voidnet proxies a request to your server, it sends the key in the `Authorization` header: ``` Authorization: Bearer vnp-sk-27fc6268... ``` **Your server must validate this header** on every request and reject invalid or missing keys with a `401` response. ### Key lifecycle [#key-lifecycle] * **Generate** — Created when you publish an app or manually from the Console * **Revoke** — You can revoke keys at any time; revoked keys immediately stop working * **Rotate** — Generate a new key, update your server, then revoke the old one * **Storage** — Keys are encrypted at rest in the Voidnet database *** ## Server Requirements [#server-requirements] Your MCP server must meet these requirements to work with Voidnet Console: ### 1. HTTPS [#1-https] All communication with the Voidnet is over HTTPS. You'll need a valid TLS certificate. ### 2. Single MCP endpoint [#2-single-mcp-endpoint] The Voidnet sends all MCP requests to a single endpoint on your server — typically `/mcp`. This single endpoint handles all MCP methods. ### 3. Protocol version [#3-protocol-version] Voidnet Console requires **MCP protocol version `2026-07-28`** (stateless). Your server should: * Accept the `MCP-Protocol-Version: 2026-07-28` header * Include `2026-07-28` in the `supportedVersions` array of the `server/discover` response * Reject unsupported versions with `400` and error code `-32022` **No sessions, no initialize handshake, no GET/DELETE endpoints.** The legacy 2025-11-25 stateful model is not supported. ### 4. Authentication [#4-authentication] Your server must validate the `Authorization: Bearer ` header and return `401` for invalid or missing keys. See [Authentication](#authentication) below. ### 5. Required headers [#5-required-headers] The Voidnet forwards these headers on every request: | Header | Description | | ---------------------- | -------------------------------------------- | | `MCP-Protocol-Version` | `2026-07-28` (gateway's protocol version) | | `Mcp-Method` | JSON-RPC method name (e.g., `tools/call`) | | `Mcp-Name` | Tool/resource/prompt name for routing | | `Authorization` | `Bearer vnp-sk-...` (your publisher API key) | Your server must **validate** that `MCP-Protocol-Version` matches the `_meta.protocolVersion` in the JSON-RPC request body. Mismatch returns `400` with error code `-32020` (HeaderMismatch). *** ## Authentication [#authentication] The authentication contract between the Voidnet and your server is: ``` Request: Authorization: Bearer vnp-sk-27fc6268... Response (valid key): 200 OK Response (invalid or missing key): 401 Unauthorized { "jsonrpc": "2.0", "error": { "code": -32001, "message": "Unauthorized", "data": "Invalid or missing publisher API key" } } ``` Validate on **every request**. If the key is invalid, return `401` immediately — do not process the MCP request. *** ## MCP Protocol (stateless 2026-07-28) [#mcp-protocol-stateless-2026-07-28] Your server must implement the following MCP 2026-07-28 methods. **No initialize handshake, no sessions, no `MCP-Session-Id` headers.** ### server/discover (REQUIRED) [#serverdiscover-required] The Voidnet calls `server/discover` to discover your server's capabilities. This replaces the legacy `initialize` handshake. **Request:** ```json { "jsonrpc": "2.0", "id": 1, "method": "server/discover" } ``` **Response:** ```json { "jsonrpc": "2.0", "id": 1, "result": { "supportedVersions": ["2026-07-28"], "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": false, "listChanged": true }, "prompts": { "listChanged": true } }, "serverInfo": { "name": "my-server", "version": "1.0.0" }, "instructions": "Optional usage instructions for your server" } } ``` * `supportedVersions`: Must include `2026-07-28` * `capabilities`: Your server's capabilities (tools, resources, prompts) * `serverInfo`: Your server's name and version * `instructions`: Optional human-readable instructions ### tools/list [#toolslist] Returns the tools your server provides: ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } ``` **Response** (include `ttlMs` and `cacheScope` for spec-sanctioned caching): ```json { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "my_tool", "description": "Does something useful", "inputSchema": { "type": "object", "properties": { "input": { "type": "string", "description": "The input value" } }, "required": ["input"] } } ], "ttlMs": 300000, "cacheScope": "private" } } ``` ### tools/call [#toolscall] Invokes a tool with the provided arguments: ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "my_tool", "arguments": { "input": "hello" } } } ``` **Response** (include `resultType` — `complete` or `input_required` for MRTR): ```json { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "Result: hello" } ], "resultType": "complete" } } ``` ### resources/list (optional) [#resourceslist-optional] ```json { "jsonrpc": "2.0", "id": 4, "method": "resources/list" } ``` **Response:** ```json { "jsonrpc": "2.0", "id": 4, "result": { "resources": [ { "uri": "data://my-resource", "name": "My Resource", "description": "A data resource", "mimeType": "text/plain" } ], "ttlMs": 300000, "cacheScope": "private" } } ``` ### resources/read (optional) [#resourcesread-optional] ```json { "jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": { "uri": "data://my-resource" } } ``` **Response:** ```json { "jsonrpc": "2.0", "id": 5, "result": { "uri": "data://my-resource", "mimeType": "text/plain", "text": "Resource content here", "ttlMs": 300000, "cacheScope": "private" } } ``` ### prompts/list (optional) [#promptslist-optional] ```json { "jsonrpc": "2.0", "id": 6, "method": "prompts/list" } ``` ### prompts/get (optional) [#promptsget-optional] ```json { "jsonrpc": "2.0", "id": 7, "method": "prompts/get", "params": { "name": "system_instruction", "arguments": { "task": "write a summary" } } } ``` ### subscriptions/listen (optional) [#subscriptionslisten-optional] For servers that support change notifications. The Voidnet calls this once per subscription: ```json { "jsonrpc": "2.0", "id": 8, "method": "subscriptions/listen", "params": { "notifications": [] } } ``` Acknowledge with `notifications/subscriptions/acknowledged`. *** ## Server Verification [#server-verification] When you publish an app in the Console, Voidnet performs a **live verification** of your server: 1. The Voidnet calls `server/discover` to discover your server's capabilities 2. It calls `tools/list`, `resources/list`, and `prompts/list` 3. The discovered capabilities are cached and displayed in the Console Your server must implement `server/discover` and the list methods above. Verification determines which tools, resources, and prompts buyers will see. You can re-verify your server at any time from the app detail page — click **Refresh** in the MCP Configuration section. *** ## Domain Verification [#domain-verification] Before you can publish an app, you must verify ownership of your server's domain: 1. **Initiate** — In the Console, start domain verification. A unique verification token is generated. 2. **Place the file** — Host the verification file at your domain's root or `/.well-known/` directory: ``` https://your-domain.com/voidnet-site-verification-.html ``` 3. **Verify** — Voidnet Console fetches the file and confirms the content matches the expected token. 4. **Done** — The domain is marked as verified and can be used for publishing. One verified domain can host multiple apps. *** ## Tier Configuration [#tier-configuration] Every published app has configurable tiers: ### Free Tier [#free-tier] * **Meter limit** — Max requests per month before the free tier is exhausted * **Rate limit per minute** — Max requests per minute * **Rate limit per day** — Max requests per day ### Paid Tier [#paid-tier] * Same limits as free tier but higher * **Price** — Set in USD with monthly, yearly, or one-time billing * Requires a connected Stripe account (see below) ### How tiers work [#how-tiers-work] 1. A buyer discovers your app in the marketplace 2. If you offer a free tier, the buyer can start using it immediately 3. If you offer a paid tier, the buyer purchases a subscription 4. The Voidnet enforces rate limits and metering based on the buyer's tier 5. When a free tier buyer exhausts their meter, they're prompted to upgrade *** ## Stripe Connect Billing [#stripe-connect-billing] To accept payments, you need a Stripe Express account connected to Voidnet Console: 1. **Connect** — In the Console Payments page, click "Connect with Stripe" 2. **Onboard** — Complete Stripe Express onboarding (identity verification, bank account) 3. **Sync** — Your Stripe account status appears in the Console ### Payout structure [#payout-structure] | Component | Share | | ---------------------------- | ------------- | | Publisher payout | 80% (default) | | Voidnet Console platform fee | 20% (default) | Per-app — the default 80/20 can be lowered on individual apps (negotiated deals, set by Voidnet on request). The rate is locked at publish time; each purchase splits from that locked rate. Payouts are sent directly by Stripe to your connected bank account on Stripe's standard payout schedule. ### Pricing model [#pricing-model] * The Voidnet uses the **Marketplace / Direct Charges** model * You are the merchant of record for each transaction * The Voidnet creates a PaymentIntent with an `application_fee` for Voidnet Console's per-app fee (default 20%) * You receive 80% (default) — rounded: `fee = floor(amount*percent/100)`, `payout = amount - fee` so the two always sum to gross exactly (platform absorbs the cent). Stripe processing fees are separate and deducted from the gross before the split. *** ## Testing Your Server [#testing-your-server] ### 1. Local development [#1-local-development] Run your MCP server locally with HTTPS (self-signed certs are fine for testing). The Console's verify-server tool supports localhost with `rejectUnauthorized: false` for external publishers. Internally, Voidnet engineers use `http` on localhost — external publishers must use `https`. ### 2. Verify server in Console [#2-verify-server-in-console] From the **Publish MCP Interface** page: 1. Enter your **App Name** (unique per publisher) 2. Select a **verified domain** — the server URL is auto-populated from your domain 3. Add a **description** 4. Click **Verify Server** 5. The Console calls `server/discover` → `tools/list` → `resources/list` → `prompts/list` and displays your capabilities 6. **Save your publisher API key** — shown once in the success dialog 7. Configure tiers and click **Submit** to publish ### 3. Test with curl [#3-test-with-curl] ```bash curl -X POST https://api.openvoidnet.com/v1-beta/mcp/your-username/your-app \ -H "Authorization: Bearer vnb-sk-your-buyer-key" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: tools/list" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' ``` *** ## Going Live [#going-live] ### Prerequisites checklist [#prerequisites-checklist] * [ ] Server is running HTTPS with a valid certificate * [ ] Server validates the `Authorization: Bearer ` header * [ ] Server implements MCP `2026-07-28` protocol (stateless) * [ ] Server implements `server/discover` (required) * [ ] Server exposes at least one tool * [ ] Domain is verified * [ ] Stripe Express account is connected (if offering paid tier) ### Publishing flow [#publishing-flow] 1. Go to **Console → Publish Apps** and select **AI Tools (MCP)** 2. Enter your **App Name** (unique per publisher, like a GitHub repo name) 3. Select a **verified domain** to set the server URL 4. Add a **description** 5. Click **Verify Server** — the Voidnet probes your MCP server with `server/discover` and discovers its capabilities 6. Review the detected **tools, resources, and prompts** 7. **Save your publisher API key** (`vnp-sk-*`) — it's shown only once in the success dialog. Without it, the Voidnet cannot proxy requests to your server 8. Close the dialog and configure **tiers** — free, paid, or both, with rate limits and metering limits 9. Click **Submit** — your app is created as a draft and immediately published to the marketplace 10. Your app now appears in **Published Apps** and the [Voidnet Console Marketplace](https://openvoidnet.com/marketplace) ### Drafts [#drafts] Every app starts as a **draft** when you first verify the server. Drafts appear in the **Publish Apps** page under "Continue your drafts" and can be resumed at any time. Publishing (Submitting) sets the status to `published`, making the app visible in the marketplace. | Status | Visible in marketplace | Modifiable | | ----------- | ---------------------- | ------------------------------------------ | | `draft` | No | Yes — can edit tiers, re-verify, or delete | | `published` | Yes | Yes — can update, re-verify, or delete | ### Managing your app [#managing-your-app] * **View details** — Console → Published Apps → click your app * **API keys** — Generate and revoke keys from the API Keys tab on the app detail page * **Logo** — Upload or remove an app logo from the app detail page (400×400 WebP, PNG, JPEG, or AVIF) * **Re-verify** — Refresh server capabilities from the Overview tab on the app detail page * **Analytics** — Track requests and revenue per app * **Delete** — Remove your app (irreversible) from either the Published Apps list or Publish Apps page *** ## Reference: Server skeleton [#reference-server-skeleton] A minimal MCP server in TypeScript/Node.js (stateless 2026-07-28): ```typescript import { createServer } from "https"; import { readFileSync } from "fs"; const PUBLISHER_API_KEY = process.env.PUBLISHER_API_KEY; const PORT = Number(process.env.PORT ?? 3051); // Read and parse the JSON-RPC request body (1 MB cap). function parseBody(req: import("http").IncomingMessage): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; let size = 0; req.on("data", (chunk: Buffer) => { size += chunk.length; if (size > 1024 * 1024) { reject(new Error("Request body too large")); req.destroy(); return; } chunks.push(chunk); }); req.on("end", () => { try { resolve(JSON.parse(Buffer.concat(chunks).toString("utf8"))); } catch (err) { reject(err); } }); req.on("error", reject); }); } const server = createServer( { key: readFileSync("./key.pem"), cert: readFileSync("./cert.pem") }, async (req, res) => { if (req.method === "GET" || req.method === "DELETE") { res.writeHead(405, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32601, message: "Method not allowed" } })); return; } // 1. Validate publisher API key const expected = `Bearer ${PUBLISHER_API_KEY}`; if (PUBLISHER_API_KEY && req.headers["authorization"] !== expected) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" } })); return; } // 2. Validate MCP-Protocol-Version header const protocolVersion = req.headers["mcp-protocol-version"] as string | undefined; if (protocolVersion !== "2026-07-28") { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32022, message: "UnsupportedProtocolVersion", data: { supported: ["2026-07-28"], requested: protocolVersion } } })); return; } // 3. Parse MCP JSON-RPC and validate _meta protocolVersion matches header const body = await parseBody(req); const metaVersion = body.params?._meta?.protocolVersion ?? body.params?._meta?.["io.modelcontextprotocol/protocolVersion"]; if (metaVersion && metaVersion !== protocolVersion) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32020, message: "HeaderMismatch" } })); return; } // 4. Dispatch in your own handleMcpRequest function — handle these for verification and store: // server/discover, tools/list, tools/call, resources/list, resources/read, // resources/templates/list, prompts/list, prompts/get, subscriptions/listen // and return the JSON-RPC result object with ttlMs/cacheScope + resultType: "complete". const response = await handleMcpRequest(body); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(response)); }); server.listen(PORT, () => console.log(`MCP listening on :${PORT}`)); ``` Direct publisher test (no gateway): ```bash curl -X POST http://localhost:3051/mcp \ -H "Authorization: Bearer vnp-sk-test..." \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: server/discover" -H "Mcp-Name: server/discover" \ -d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}' ``` > **Coming next:** `@openvoidnet/mcp-sdk` will wrap `McpServer` + `HttpTransport` so you don't copy this skeleton. See the [Voidnet test server](https://github.com/void-labs/mcp-test-server-https) for a complete reference implementation. # Usage and Reconciliation (/docs/publisher-guide/usage) Every call made through Voidnet appears in your dashboard so you can count it yourself and compare. ## How counting works [#how-counting-works] A request is counted when a buyer successfully calls a billable tool on your server. The Console calls this a request. List and discover calls are not counted. If the tool returns an error, it is still shown in the log so you can see what happened, but it is not billed. Each counted request records a request ID, the time it was received, the tool that was called, how long your server took to respond, and whether the buyer was on a free or paid tier. ## Where to see it [#where-to-see-it] Open **Console → Published Apps → your app → Usage Logs**. The table lists every counted request for that app, most recent first. You can copy the request ID and match it to the ID in your own server logs. The time column uses the gateway's clock — compare it to your server's clock with a small window for clock drift. Use the pagination controls to page through older calls and **Export CSV** to download the current page for offline comparison. ## Reconciling with your own logs [#reconciling-with-your-own-logs] Your server logs the same request ID on every call forwarded from Voidnet. To reconcile: 1. Log the request ID and time on your server for each tool call. 2. Open the Usage Logs tab for that app. 3. Filter or page to the same time window. 4. Compare counts. Every counted call appears in the log. There are no hidden states — if a call is counted, it is listed. If it is not listed, it was not counted. If counts differ, check for calls that returned an error or were filtered as non-billable in the docs. If you still see a mismatch, contact support with the request IDs and times. ## API access [#api-access] The same data is available via a publisher API for programmatic reconciliation: ``` GET /api/console/publishers/apps/{appId}/metering?limit=50&offset=0&from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z ``` The API requires a console session. It returns `records` with `requestId`, `timestamp`, `appName`, `toolName`, `operationType`, `tier`, `requests`, `durationMs`, `error`, and `buyerId`, plus `pagination` with `total`. ## Notes [#notes] Counts are final once they appear in the log. The `v1-beta` endpoint family is versioned, and any future change to counting will be announced in the changelog with a deprecation window. # API Reference (/docs/reference/api-reference) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} - [Mcp](https://docs.openvoidnet.com/docs/reference/api/mcp): MCP tools (JSON-RPC 2.0, stateless 2026-07-28) — call tools; only tools/call is billable. - [Llm](https://docs.openvoidnet.com/docs/reference/api/llm): AI models (OpenAI-compatible, TEXT only, stream + non-stream) — chat completions and models listing. - [Health](https://docs.openvoidnet.com/docs/reference/api/health): Gateway and dependency (database, redis) health. # Error Reference (/docs/reference/error-reference) Every gateway error uses one JSON shape. The `error` field is an **object** with `code`, `message`, and `status` — not a string. ```json { "error": { "code": "api_key_invalid", "message": "Invalid token format. Expected: vnb-sk-xxx or JWT", "status": 401 } } ``` OAuth endpoints (`POST /oauth/token`, `GET/POST /authorize`, `/.well-known/*`) use a **different** shape: `{"error": "", "error_description": ""}`. These are noted below. *** ## Authentication — `401` [#authentication--401] Returned when the `Authorization` header is missing, malformed, or the credential is unknown/expired. | Code | Condition | Fix | | ----------------- | ------------------------------------------------------------------- | --------------------------------------------------- | | `api_key_missing` | No `Authorization` header | Add `Authorization: Bearer ` | | `api_key_invalid` | Key format is wrong, JWT header/signature invalid, or key not found | Key prefixes: `vnb-sk-` or a 3-segment JWT | | `api_key_expired` | JWT access token expired | Request a new token from `POST /oauth/token` | | `api_key_revoked` | Key was revoked in the Console | Generate a new key | | `buyer_not_found` | JWT is valid but the buyer no longer exists | Request a new token; contact support if it persists | The gateway auto-detects the credential type: a 3-segment (dot-separated) value is treated as a JWT; a `vnb-sk-` prefix is treated as an API key. WWW-Authenticate header is set on `401` (missing/invalid/expired key) and `403 insufficient_scope` responses so OAuth-aware clients can bootstrap discovery. ## OAuth — `400` / `401` / `405` / `500` [#oauth--400--401--405--500] Returned by `POST /oauth/token`, `GET/POST /authorize`, and the `/.well-known/*` metadata endpoints. **These use the OAuth error shape, not the gateway envelope.** | Status | Code | Condition | | ------ | --------------------------- | ----------------------------------------------------------------------------------------------- | | 400 | `invalid_request` | Body unreadable or Content-Type is not `application/x-www-form-urlencoded` / `application/json` | | 400 | `unsupported_grant_type` | `grant_type` is not `client_credentials`, `authorization_code`, or `refresh_token` | | 400 | `invalid_client` | `client_id` empty, bad prefix, or `client_secret` mismatch | | 400 | `invalid_grant` | Authorization code invalid/expired, PKCE mismatch, refresh token reused/rotated | | 400 | `unsupported_response_type` | `response_type` is not `code` | | 400 | `invalid_request` | Missing required params (`client_id`, `redirect_uri`, `code_challenge`, etc.) | | 401 | `invalid_client` | Credential lookup failed (key not found) | | 405 | `unsupported_method` | Wrong HTTP method on a `/.well-known/*`, `/oauth/token`, or `/authorize` endpoint | | 500 | `server_error` | Token signing not configured or signing failed | | 404 | `not_found` | JWKS requested but signing not configured (on `/.well-known/jwks.json`) | ## Protocol — `400` [#protocol--400] Returned for MCP protocol version mismatches and header validation. | Status | Code | Condition | Fix | | ------ | --------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | 400 | `UnsupportedProtocolVersion` | `MCP-Protocol-Version` header not `2026-07-28` | Use `2026-07-28` (MCP only) | | 400 | `HeaderMismatch` | `MCP-Protocol-Version` header != `_meta.protocolVersion` in request body | Make both match `2026-07-28` (MCP only) | | 400 | `MissingRequiredClientCapability` | Client lacks a capability required by the method | Add capability in `_meta.clientCapabilities` (MCP only) | | 400 | `invalid_request` | Missing required headers (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`) | Add all three headers (MCP only) | | 400 | `invalid_request` | LLM `model` or `messages` missing, or invalid JSON | Include `{"model":"...","messages":[{"role":"user","content":"..."}]}` (LLM only) | | 400 | `invalid_request` | LLM multimodal `image_url/audio/video` present | TEXT only — send `content: string` only (LLM) | | 502 | `publisher_error` | LLM stream ended with no `usage.total_tokens` chunk | Publisher must send a final usage chunk (LLM streaming) | ## Billing — `402` / `403` / `429` [#billing--402--403--429] Returned after authentication, when the purchase/subscription check or wallet deduct fails. | Status | Code | Condition | Fix | | ------ | ------------------------ | ----------------------------------------- | ---------------------------------------------------------------- | | 402 | `insufficient_balance` | Wallet balance can't cover the call price | Top up at **Console → Wallet** ($5–$1000 via Stripe), then retry | | 403 | `app_not_purchased` | No purchase or subscription record | Purchase (or "Get" a free tier) in the Marketplace | | 403 | `subscription_inactive` | Subscription exists but isn't active | Check billing status in the Console | | 403 | `subscription_expired` | Subscription period ended | Renew in the Console | | 403 | `subscription_cancelled` | Subscription was cancelled | Contact support or repurchase | | 429 | `meter_expired` | Billing period expired, meter needs reset | Wait for meter reset | ## Metering — `400` / `501` [#metering--400--501] Returned when the gateway can't record usage for the call. Rare — the call itself reached the publisher. | Status | Code | Condition | Fix | | ------ | ------------------------ | ---------------------------------------- | -------------------------------------------- | | 400 | `invalid_record` | Metering record failed validation | Retry; contact support if it persists | | 400 | `invalid_timestamp` | Metering timestamp rejected | Retry; contact support if it persists | | 501 | `protocol_not_supported` | A2A metering attempted — A2A is not live | A2A is not available yet; use `mcp` or `llm` | ## Rate limiting — `429` [#rate-limiting--429] Returned when a usage limit is exceeded. All limits use fixed-window counters. | Status | Code | Condition | Fix | | ------ | ---------------------- | --------------------------------------------- | ----------------------------------------------------- | | 429 | `rate_limit_exceeded` | Request rate exceeded (per-minute or per-day) | Wait for the window to reset, then retry with backoff | | 429 | `meter_limit_exceeded` | Monthly request quota exhausted | Wait for the rolling 30-day reset, or upgrade tier | `429` responses do **not** include a `details` payload. There are no `X-RateLimit-*` response headers. Track your usage in the Console. ## Suspension / Scope — `403` [#suspension--scope--403] | Status | Code | Condition | | ------ | --------------------- | -------------------------------------- | | 403 | `buyer_suspended` | Buyer account suspended | | 403 | `publisher_suspended` | Publisher account suspended | | 403 | `insufficient_scope` | JWT scope doesn't permit the operation | ## Routing — `400` / `404` [#routing--400--404] | Status | Code | Condition | Fix | | ------ | ----------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | | 400 | `invalid_path` | URL doesn't match `/v1-beta/{adapter}/{username}/{appname}` | Check the path | | 400 | `invalid_adapter_type` | Defensive only — unknown adapters never reach validation (routing returns `404 not_found` first) | Adapter segment is `mcp` or `llm` | | 400 | `invalid_adapter` | Defensive only — route and adapter always match after routing | Use `mcp` for tools, `llm` for chat | | 400 | `invalid_request` | LLM missing `model`/`messages` or multimodal | `model` and `messages: [{role, content:string}]` required, TEXT only | | 400 | `invalid_request` | Missing fields or invalid JSON-RPC structure | Check the JSON-RPC body | | 404 | `app_not_found` | No app registered for that username/appname | Verify both names | | 404 | `publisher_not_found` | Publisher username doesn't exist | Check the username | | 404 | `publisher_api_key_not_found` | Publisher has no active API key | Publisher must regenerate their key | | 404 | `purchase_not_found` | Purchase record missing during usage query | Confirm the purchase | ## Publisher — `4xx` / `5xx` [#publisher--4xx--5xx] Returned when the gateway proxies to the publisher and the publisher's server responds with an error. | Status | Code | Condition | | | ------ | ------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | 4xx | `publisher_client_error` | Publisher returned a 400–499 (their error — check their docs) | | | 5xx | `publisher_server_error` | Publisher returned a 500+ (their server error) | | | 502 | `publisher_error` | Publisher transport/initialization failure | | | 502 | `app_offline` | App server unreachable — circuit opened after repeated failures | Retry shortly; contact the publisher if it persists | | 500 | `publisher_not_payable` | Paid call blocked — publisher has no connected payout account | Contact the publisher | | 504 | `gateway_timeout` | Publisher didn't respond in time | | | 501 | `adapter_not_supported` | Requested adapter isn't available (available: `mcp`, `llm`) | | | 502 | `publisher_error` (LLM) | Publisher returned invalid JSON or missing `usage.total_tokens` | Publisher must return `{"choices":[{"message":{"content":"..."} }],"usage":{"total_tokens":...}}` | ## Request — `400` / `404` / `405` [#request--400--404--405] | Status | Code | Condition | | | ------ | -------------------- | --------------------------------------------------------------------------------------- | ------------------------------------- | | 400 | `bad_request` | Unparseable request | | | 413 | `body_too_large` | Request body exceeds the size limit | | | 404 | `not_found` | No route matches the URL | | | 405 | `method_not_allowed` | PUT/DELETE on a known route (GET routes to the pipeline; POST is the only write method) | Use POST (PUT/DELETE are never valid) | ## Internal — `500` [#internal--500] | Status | Code | Condition | | ------ | -------------------- | ----------------------------------------------- | | 500 | `internal_error` | Server-side error — retry, then contact support | | 500 | `database_error` | Database query/write failed | | 500 | `redis_error` | Rate-limit cache operation failed | | 500 | `trace_error` | Failed to start request tracing (non-critical) | | 500 | `invalid_app_config` | App configuration is invalid | # Rate Limiting (/docs/reference/rate-limiting) The Voidnet enforces usage limits so that no single buyer or key can overload the system or a publisher's server, and so publishers can offer free and paid tiers with monthly quotas. ## Limits [#limits] Your request volume is capped in two ways: * **Rate limit** — a per-minute and per-day cap on how fast you can send requests. Exceeding it returns `429 rate_limit_exceeded`. * **Meter limit** — a monthly quota on total requests per app. Defined by the publisher per tier. Exceeding it returns `429 meter_limit_exceeded`. Meters reset on a rolling 30-day window from your purchase date. When a billing period ends, the gateway returns `429 meter_expired` until the meter resets. ## What happens when you hit a limit [#what-happens-when-you-hit-a-limit] A `429` response looks like: ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded: too many requests per minute", "status": 429 } } ``` ```json { "error": { "code": "meter_limit_exceeded", "message": "Monthly meter limit exceeded", "status": 429 } } ``` There are no `X-RateLimit-*` response headers. Track your usage and remaining quota in the Console under **Usage**. ## Meter limits by tier [#meter-limits-by-tier] | Tier | Typical monthly meter | Purpose | | ---- | --------------------------------- | -------------------- | | Free | 1,000 requests | Trial and evaluation | | Paid | Set by publisher (often 100,000+) | Production usage | Publishers configure these limits when they set up an app's tiers. To see an app's limits, check its marketplace page. ## Fairness between apps and tiers [#fairness-between-apps-and-tiers] Buckets belong to apps: your usage of one app never consumes another app's quota. Free and paid traffic are counted separately at every fairness scope, so free-tier bursts can't eat paid headroom. Paid traffic skips the publisher-capacity check entirely — it is already bounded per app by tier quotas and wallet balance. The publisher's server has its own capacity ceiling (per publisher, free traffic only) — one hot app can't throttle its siblings. ## Best practices [#best-practices] 1. **Implement exponential backoff** — on a `429`, wait before retrying. Start at 1 second, double each attempt. 2. **Track usage in the Console** — the **Usage** page shows remaining meter quota per app so you can anticipate limits. 3. **Distribute across keys** — for high-volume applications, use multiple API keys to spread load. 4. **Upgrade your tier** — if you consistently hit limits, a paid tier from the publisher gives a higher meter. # Health (/docs/reference/api/health) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} ## Endpoints - `GET /health` - `GET /health/live` # Llm (/docs/reference/api/llm) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} ## Endpoints - `GET /v1-beta/llm/{username}/{appname}` - `POST /v1-beta/llm/{username}/{appname}` # Mcp (/docs/reference/api/mcp) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} ## Endpoints - `GET /.well-known/jwks.json` - `GET /.well-known/oauth-authorization-server` - `GET /.well-known/oauth-protected-resource` - `GET /authorize` - `POST /authorize` - `POST /oauth/token` - `POST /v1-beta/mcp/{username}/{appname}`