# 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.

<Callout>
  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=<old_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 <credential>` | 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<string> {
  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.
