proofkit.
Marketplace Network Operators Proofs Speedtest Docs Sign in
Operators

Operator documentation

Operator API v1 · revision 1.1 · 30 October 2026 · What changed

Run a node on your own server, by hand or with an AI agent: the quick start, the MCP server that ChatGPT, Claude, Claude Code, Cursor, Codex and VS Code sign in to, the API for scripts, what each node check means and how to fix it, and how earnings reach your wallet.

Start with the quick start API explorer
  1. 01Quick startFrom a deposit to a node that carries users: the stake, the check of the server, the install, the network's checks and the review.7 sections
  2. 02AI agents (MCP)ChatGPT, Claude, Claude Code, Cursor, Codex or VS Code over MCP: sign in once, then run your nodes in a conversation.7 sections
  3. 03APIKeys and scopes, errors and limits, and every path of the REST API your scripts call.24 sections
  4. 04Node checksWhat each check of a node means, why it matters and the commands that fix it.15 sections
  5. 05PayoutsWhat a node earns, how a claim reaches your wallet, and how the stake and the rest of your deposit come back.7 sections

No account yet? Become an operator · Need more? Talk to us · About running a node · Operator guide

// contents
/

No section title matches. Your browser’s find (Ctrl+F, or ⌘F on a Mac) searches the text itself.

  1. 01 /Quick start
    • What you need
    • Deposit the stake
    • Check the server
    • Register and install
    • Read the checks
    • Review and going live
    • With an AI agent
  2. 02 /AI agents (MCP)
    • What it is
    • Connect
    • Tools
    • What a key cannot do
    • The install command
    • Limits
    • If a call is refused
  3. 03 /API
    • Keys and scopes
    • Answers and errors
    • Limits
    • Revisions
    • /me
    • /wanted
    • /requirements
    • /earnings
    • /balance
    • /payouts
    • /nodes
    • /nodes/{id}
    • /nodes/{id}/checks
    • /nodes/{id}/checks/run
    • /nodes/{id}/metrics
    • /nodes/{id}/events
    • /nodes/{id}/reinstall
    • /nodes/{id}/restart
    • /nodes/{id}/benchmark
    • /links
    • /links/{id}
    • /links/{id}/tunnel
    • /nodes/{id}/invitations
    • /nodes/{id}/invitations/{iid}
  4. 04 /Node checks
    • agent_never_connected
    • agent_offline
    • agent_outdated
    • disk_low
    • cover_site
    • transport_unreachable
    • exit_probe_failing
    • host_tuning
    • overloaded
    • entry_unlinked
    • carrier_account_error
    • price_above_ceiling
    • in_review
    • paused
    • suspended
  5. 05 /Payouts
    • What you earn
    • Pending and claimable
    • Claiming
    • The stake
    • Withdrawing
    • The wallet
    • What a script or an agent can see
  6. 06 /Changes
    • 1.1 (2026-10-30)
    • 1.0 (2026-10-28)

01 /Quick start

This guide takes a server from nothing to a node that carries users. The path is the same by hand and with an AI agent: put the stake in, check the server, register the node and install the agent, read the network's checks, and wait for the review. AI agents (MCP) has the agent's side, Node checks says what each check means and how to fix it, and Payouts says how earnings reach your wallet.

What you need

  • A server with a public IPv4 address, root access and Linux on x86-64, kernel 5.6 or newer. It needs 1 vCPU and 1 GB of RAM at least (2 and 2 are recommended), 1 GB of free disk, and port 443 free on TCP and UDP. ARM servers are not supported. One server runs one node, an exit or an entry.
  • Docker, on any distribution: the installer adds it when it is missing. Without Docker the agent runs as a systemd service, which the dashboard offers for AlmaLinux and CentOS Stream 10 or newer.
  • A TON wallet with TonConnect, such as Tonkeeper, for the stake and the payouts, and 1 TON for each node you plan to run. Keep some TON in the wallet itself too: a claim or a withdrawal needs at least 0.08 TON there for gas.
  • An operator account. Create one, choose Run a node when you sign up, and enter the six-digit code that is emailed to you. A user account cannot be switched to an operator account, and an operator account cannot connect to the VPN.

Deposit the stake

Registering a node locks 1 TON of your balance as its stake, so the deposit comes first. On the Wallet page link your wallet and deposit at least 1 TON for each node you plan to run; the page takes deposits from 0.5 TON. The stake keeps the network to people who mean it. It comes back to your balance when you delete the node, less anything that was slashed: see The stake.

Check the server

Ask the server whether a node fits before you register one. The check reads the machine and changes nothing, and it needs no token and no root.

curl -fsSL https://proofkit.org/install.sh | bash -s -- --check --type=exit

Use --type=entry for an entry. It prints the architecture, the kernel, the RAM, the free disk, whether TCP and UDP 443 are free and which of Docker, Podman and systemd are there, then says what is missing. For the Docker install, which is the default, it also checks that the kernel is 5.6 or newer, and that it has the module Docker's network needs, xt_addrtype. AlmaLinux, CentOS Stream and RHEL 10 ship it in kernel-modules-extra, which a server may lack for the kernel it runs, and the check then says how to install it; --mode=systemd leaves that check out. It exits 0 when the server meets the minimum for the type and 1 when it does not, and --json prints the same facts as one JSON object. The minimum is the one GET /requirements gives, which needs no key: see /requirements.

Register and install

Open the operator dashboard and press Add node. Choose the type, exit or entry, and enter the server's public IPv4 address, its country and your price per GB. An exit costs at least 0.001 TON per GB, while an entry may be free. Users see your price with the 20% network fee added, and you receive 0.96 times it. The rest can wait: a limit on concurrent sessions, a cover site, WARP, Torrent Guard, the tunnel type and private mode are all in the node's Settings. Before you pick a country, see where the network needs nodes.

The dashboard then shows the install command with the node's token in it. SSH into the server and run it as root (the line is a pipe, so it works behind sudo too):

curl -fsSL https://proofkit.org/install.sh | bash -s -- --mode=docker --type=origin --token=YOUR_TOKEN

For an entry the type is edge. The token is a secret: anyone who has it can run an agent as your node, so do not paste the command into a chat that other people can read. If you lose it, Reinstall makes a new one.

The installer checks the kernel, and stops with the same words as the check when the kernel is older than 5.6 or lacks the module Docker's network needs. It also stops, before it changes anything, on a server that already runs the agent as a systemd service: that node is reinstalled with its install_command_systemd. It installs Docker if it is missing, opens the ports in the server's own firewall and starts the agent with an updater that looks for a new version every 6 hours. A firewall in your hosting panel, a security group for example, is separate: open the same ports there, or the network's tests will fail and users will connect and move nothing. The ports are listed in the operator guide. The node turns active when its agent's first heartbeat arrives.

Read the checks

The network now tests the node from outside. It dials the node's lines the way a user's app would, and it reads the agent's own reports: the connection, the disk, the version, the cover site and the host's network limits. Open the node's page: the Checks tab is the first one. Each check says what ProofKit saw and, when something is wrong, how to fix it, with the commands to run on the server. Check again asks for a new round of test connections, once in 5 minutes for a node.

The same list is GET /nodes/{id}/checks in the API, and ProofKit writes to you when a check has failed for a while, by email and in Telegram if you linked it. Node checks has a section for each check.

Review and going live

A public node is reviewed by ProofKit before the network routes users to it: until then users reach it only when they choose it themselves, and the review starts once no check holds the node back. A private node needs no review, and an entry also needs a link to an exit. When the node starts carrying users its state reads live, and ProofKit writes to you if the alert Node live is on. Earnings then show up as users' traffic is billed, and Payouts says how to claim them.

With an AI agent

An agent can do the registration and the install for you. Connect it first, as AI agents (MCP) says: signing in is the way for ChatGPT, Claude, Claude Code, Cursor, Codex and VS Code, and a key is the way for a script. Then give it the server and the price:

Put a ProofKit exit on [email protected]. It is in Brazil. 0.003 TON per GB.

The agent works through the same steps. get_account says whom it works for and whether your balance covers the stake: if it does not, the deposit is yours to make, because no key and no agent can move money. wanted_locations and server_requirements say where a node helps and what the server needs. Over SSH it runs the check from above, then create_node registers the node, locks the stake and returns the install command, which the agent runs over SSH or hands to you when it has no SSH. node_checks reads the checks, with run_now to ask for a new round, and each failing one comes with its fix. ProofKit's review is the one step no agent can do for you.

Back to contents

02 /AI agents (MCP)

This guide connects an AI agent to your operator account: ChatGPT, Claude, a coding agent such as Claude Code, Cursor or Codex, a VS Code chat, or a script. The agent works on your nodes in a conversation. It reads the account and the balance, registers a server as a node and hands you the command that installs it, reads the node's checks and says what to fix, changes a node's settings, links an entry to an exit, invites someone to a private node, and reads the metrics, the events and the payouts. It does what the Operator API does, with the same rules, limits and refusals, because each tool is one call of that API. You connect an agent by signing in on proofkit.org and pressing Allow, with no key to copy; a script, or an agent that cannot sign in, uses an operator API key that you make yourself. You can end either connection at once.

What it is

  • The address is https://proofkit.org/mcp/operator, with no trailing slash. It speaks MCP over HTTP and keeps no session: every request is a POST that gets one JSON answer. A browser that opens the address is shown a page on how to connect, and any other GET is refused. Any client that can send a header to an HTTP MCP server can connect.
  • Two ways in. Signing in is for an assistant that has a sign-in of its own, which ChatGPT, Claude, Claude Code, Cursor, Codex and VS Code all have: the assistant opens proofkit.org in your browser, you sign in to your operator account and press Allow, and the assistant works within what you allowed until you disconnect it. A key is an operator API key, the pko_… one, for a script, a bot, or an agent that cannot sign in: it goes in the header Authorization: Bearer pko_…. Every request needs a sign-in or a key, initialize and the list of tools included. Either reaches through this server what it reaches on the REST API, and nothing more (see What a key cannot do).
  • Who can sign in. The operator account, with its email address confirmed: until it is, a box at the top of the Agents page sends a six-digit code to the address and checks it. The account needs no second factor to connect, as a key needs none to be used; when it has one, the Allow page asks for its code unless you passed it when you signed in.
  • Who is let in. While the server is opened account by account, a request of an account that is not on the list gets 404, as if there were no server there, whether it comes with a sign-in or with a key.
  • The protocol. The server speaks both generations of MCP on this one address: the revisions that open with initialize, which most clients use today, and 2026-07-28. A client needs no setting for it.
  • What the agent is told. When it connects, the server gives it eight lines: what the tools are for; the path to a new node (check the balance, register it, install over SSH, read the checks and fix what they name); that registering locks a stake from the balance; that ProofKit reviews a public node before it takes users and that no tool approves one; that a payout is your own press of Claim on the Wallet page and that no tool moves money; what no key or connected app can do; to ask you before it deletes a node, unlinks nodes or revokes an invitation; and, last, where these guides are, https://proofkit.org/operator-docs.
  • What ProofKit keeps. For each assistant you connect: its client's name, the address it returns to, what you allowed and the dates it was connected and last used. For 30 days, each call of a tool leaves a line in a log, in the name of the assistant or the key that made it (a key by its first characters, an assistant by its name), with the tool, the node when the call names one, how it ended and when; the newest 1,000 calls of each key and each assistant are kept. No address and no other argument. The Agents page shows the connected assistants and the last calls. What you write to an agent is processed by that agent's provider, not by us.

Connect

Open the Agents page (https://proofkit.org/#/operator/agents): under Connect your agent it has a button or a command for each assistant below, with the address filled in. Every example connects the server under the name proofkit-operator.

Sign in

In each of the assistants below the sign-in starts by itself, and your browser opens the Allow page on proofkit.org:

  • It names the assistant and the address it returns to after you press Allow: read the address, because a name is only what the assistant calls itself.
  • It names your operator account and the address you are signed in as. Sign in first if you are not, and give the second-factor code if it asks.
  • It shows two permissions. Read your nodes, their checks, your earnings and your balance is always on. Manage, to register, change, restart and delete your nodes and change their prices, is a box that appears, ticked, when the assistant asks for it: untick it to let the assistant only look.
  • It states what no assistant can do: move money, make or revoke your API keys, change your password, email or two-factor sign-in, or delete your account.

Press Allow and the assistant is connected. Press Deny and it is told you refused. Connecting the same assistant again from the same place replaces its earlier connection. Each install of a desktop assistant is a connection of its own: Claude Code on two machines is two connections, and Disconnect ends one of them.

ChatGPT

Open chatgpt.com/plugins, press + and choose Add custom MCP server. Paste the address https://proofkit.org/mcp/operator and choose OAuth for authentication. Press Create as a plugin: ChatGPT opens proofkit.org, where you sign in and press Allow. ChatGPT has no prefilled link.

Claude

On claude.ai or in the Claude desktop app, press Add to Claude on the Agents page, or open this link: https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=ProofKit%20Operator&connectorUrl=https%3A%2F%2Fproofkit.org%2Fmcp%2Foperator. It fills in the name and the address. By hand, it is Settings → Connectors → Add custom connector, the name ProofKit Operator and the address. Add the connector: Claude opens proofkit.org, where you sign in and press Allow. Where Claude asks how people sign in, choose Sign in now.

Claude Code
claude mcp add --transport http proofkit-operator https://proofkit.org/mcp/operator

Run /mcp in Claude Code, choose proofkit-operator and follow the prompt to sign in: your browser opens proofkit.org, where you sign in and press Allow. Do not add an Authorization header to the server: Claude Code does not start a sign-in for a server that has one.

Cursor

Press Add to Cursor on the Agents page and press Install when Cursor asks. By hand, in .cursor/mcp.json, or ~/.cursor/mcp.json for every project:

{
  "mcpServers": {
    "proofkit-operator": {
      "url": "https://proofkit.org/mcp/operator"
    }
  }
}

In Cursor's MCP settings, press Connect next to proofkit-operator: your browser opens proofkit.org, where you sign in and press Allow.

Codex
codex mcp add proofkit-operator --url https://proofkit.org/mcp/operator

Codex starts the sign-in by itself: your browser opens proofkit.org, where you sign in and press Allow. If the browser does not open, run codex mcp login proofkit-operator. The same server in ~/.codex/config.toml:

[mcp_servers.proofkit-operator]
url = "https://proofkit.org/mcp/operator"
VS Code

Press Add to VS Code on the Agents page and press Install when VS Code asks; if your browser does not hand the link to VS Code, press Add through vscode.dev beside it. By hand, in .vscode/mcp.json, or the user-level mcp.json:

{
  "servers": {
    "proofkit-operator": {
      "type": "http",
      "url": "https://proofkit.org/mcp/operator"
    }
  }
}

When VS Code asks to sign in to the server, accept: your browser opens proofkit.org, where you sign in and press Allow.

Check it

Ask the agent who it works for. It calls get_account and answers with your account, its balance, whether the balance covers the stake of a new node, and the key it is using, if it uses one.

Read and manage

What an assistant may do is what you ticked on the Allow page. Read lets it look: the account, the balance, the nodes and their checks, the metrics and events, the links and invitations, the earnings and the payouts. Manage lets it change things: register, change, reinstall, restart and delete nodes, link and unlink them, and make and revoke invitations. An assistant connected with Read alone is not even shown the tools that change something, and a call to one is refused with HTTP 403 and INSUFFICIENT_SCOPE, with the challenge a client asks you for more by. To give it more, connect it again and leave Manage ticked.

Every call is checked again as it is made: the account must still be an operator and still be on the list, and it is held to the same limits as a key (see Limits). An assistant cannot do what you could not do on the dashboard.

How long a connection lasts

An assistant renews its sign-in by itself, so you are not asked again. It stops working when:

  • you disconnect it (below);
  • the account's sessions are ended: you reset your password, turn a second factor on or off (the authenticator, or Telegram's), unlink Telegram, or sign out everywhere (each of these ends every session of the account, and with them every connection of the account, at its next call, so connect the assistants again; a key is not affected);
  • its renewal token is used twice, which means a copy of it exists: ProofKit ends the connection at once;
  • it goes 60 days without a call;
  • the account is not an operator account any more.

Having no second factor does not end a connection, as it does not end a key: a connection asks none. But turning one on or off does, as the list above says, because it ends the account's sessions. The Agents page tells an account without a second factor to turn one on: do it before you connect the assistants, or connect them again afterwards. An account that is taken off the list gets 404 from this server, with a sign-in as with a key, and its connections work again when it is put back.

Disconnecting

On the Agents page, Connected apps lists each connected assistant with its name, where it returns to, Read or Read and manage, when it was connected and last used, and its calls of the last 30 days. Disconnect asks first and ends the connection at once: the assistant's next call is refused, on either of our servers, because nothing about a connection is cached, and it has to sign in again. Recent calls at the end of the page shows the last calls of keys and assistants together, newest first, each with who made it, what it was and the node when it named one.

When an assistant is connected, ProofKit tells you: an email to the account's address, a message on Telegram where it is linked, and a notification in the bell. It names the assistant, where it returns to, what it may do and when, and it cannot be turned off. If you did not connect it, disconnect it on the Agents page and change your password.

When sign-in fails

  • The Allow page says the account is not an operator account. Sign out and sign in with your operator account, then start the connection again from the assistant.
  • It says to confirm your email address. Open the Agents page in another tab: a box at the top asks for the six-digit code it sends to your address. Come back to the Allow page and press Check again. The request waits for 30 minutes.
  • It says the assistant server is not open for the account yet. The server is opened account by account. Ask support to open it for yours.
  • It asks for a code. The account has a second factor, and this session did not pass it when you signed in. Enter the code from your authenticator app, or the one that ProofKit sends to Telegram when you press Allow.
  • It says the request has expired or was already answered. A sign-in request lives 30 minutes and is answered once: a second tab or a double press finds it gone. Start the connection again from the assistant.
  • The Allow page never opens, or the assistant reports a connection error. The sign-in server is its own address, https://auth.proofkit.org. The assistant, or the machine it runs on, must be able to reach it and https://proofkit.org. Allow both in a company proxy or firewall, or connect a script with a key.
  • Claude Code starts no sign-in. Remove an Authorization header from the server's configuration and run /mcp again.
  • It worked and stopped, with INVALID_TOKEN. The connection ended: see How long a connection lasts. Sign in again.
  • It answers INSUFFICIENT_SCOPE. The assistant was connected with Read and asked for a tool that changes something. Connect it again and leave Manage ticked.

With a key, for scripts

An operator API key connects a script or a bot, or an agent that cannot sign in, with no sign-in. You make it on the Agents page while you are signed in to your operator account, with the account's second factor if it has one. It is shown once, when it is made: keep it in the agent's own settings or in the environment, never in a file that is committed. A key has the scope read, which every key has, or read and manage; it expires in 30 days, 90 days, a year or never; an account holds at most 10 live ones. Making a key sends the account a letter, and a Telegram message where Telegram is linked. Revoke a key on the same page: the agent's next call is refused, on either of our servers, because nothing about a key is cached. A key goes in the header Authorization: Bearer pko_… on every request. Keep it out of files that are committed: where a client can, the examples read it from the environment or ask for it.

Claude Code
claude mcp add --transport http proofkit-operator https://proofkit.org/mcp/operator --header "Authorization: Bearer pko_…"

Run /mcp in Claude Code: the server is listed, with its tools. The command keeps the key in Claude Code's own configuration, outside your project. Do not add --scope project: that writes the key into .mcp.json, a file meant to be committed with the project. With the key in a header, Claude Code starts no sign-in.

Cursor

In .cursor/mcp.json, or ~/.cursor/mcp.json for every project:

{
  "mcpServers": {
    "proofkit-operator": {
      "url": "https://proofkit.org/mcp/operator",
      "headers": { "Authorization": "Bearer ${env:PROOFKIT_OPERATOR_KEY}" }
    }
  }
}

Set PROOFKIT_OPERATOR_KEY in the environment Cursor is started from.

Codex
codex mcp add proofkit-operator --url https://proofkit.org/mcp/operator --bearer-token-env-var PROOFKIT_OPERATOR_KEY

--bearer-token-env-var names the environment variable that holds the key, and Codex sends its value as Authorization: Bearer <key>. Always give it when you use a key: without it codex mcp add --url starts a sign-in instead. Set PROOFKIT_OPERATOR_KEY in the environment Codex is started from. The same server in ~/.codex/config.toml:

[mcp_servers.proofkit-operator]
url = "https://proofkit.org/mcp/operator"
bearer_token_env_var = "PROOFKIT_OPERATOR_KEY"
VS Code

In .vscode/mcp.json, or the user-level mcp.json:

{
  "inputs": [
    {
      "type": "promptString",
      "id": "proofkit-operator-key",
      "description": "ProofKit operator API key",
      "password": true
    }
  ],
  "servers": {
    "proofkit-operator": {
      "type": "http",
      "url": "https://proofkit.org/mcp/operator",
      "headers": { "Authorization": "Bearer ${input:proofkit-operator-key}" }
    }
  }
}

VS Code asks for the key when it first starts the server, hides what you type and does not write it into the file.

A script

Any MCP client library works. By hand, a call is one POST:

curl -s https://proofkit.org/mcp/operator \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_account","arguments":{}}}'

The answer is one JSON-RPC response. A tool's result is in result: content holds a sentence and then the same data as JSON text, and structuredContent holds it as an object.

When a key is refused
  • 401 with KEY_REQUIRED. The client sent no Authorization header. Check the header in its settings, and that the environment variable it reads is set where the client was started.
  • 401 with INVALID_OPERATOR_KEY. An Authorization: Bearer pko_… that is not a good key: wrong, cut short, expired or revoked, of an account that is not an operator now, or beside a second Authorization header. Make a new key.
  • 401 with INVALID_TOKEN. What the client sent is not Bearer pko_… at all: an empty or unexpanded variable, another scheme, the key in capitals or with no Bearer, a partner key, a token of another server. The sentence says the sign-in has ended; for a key, check what the client sends.
  • 404 with NOT_FOUND. The server is not open for your account.

Tools

The server lists 22 tools: 13 that only read and 9 that change something. A key or an assistant with read is shown the 13, and one with manage all 22. A call of a tool that the key or the assistant may not call is refused with INSUFFICIENT_SCOPE and never runs (see If a call is refused). wanted_locations and server_requirements need no key on the REST API; here they are read tools, because every request to this server carries a key or a sign-in. A tool is one operation of the Operator API, so its arguments and its answer are that operation's, as the document https://proofkit.org/operator-api/openapi.json gives them: a node is named by node_id, a link by link_id and an invitation by invitation_id. A tool answers with a sentence, then the same data as JSON text, and as structuredContent under the tool's outputSchema; an answer that is a list comes wrapped in an object (nodes, locations, events, links, invitations or payouts).

ToolWhat it doesScope
get_accountThe account, its balance, the stake a new node locks and whether the balance covers it, and the key in use when the agent uses one.read
wanted_locationsWhere the network needs nodes: the type (entry or exit), the country and why.read
server_requirementsWhat a server needs to run an entry or an exit: its minimum and recommended size, ports, architectures, kernel version and modules, and a command that checks a server before you register it. Pass the type.read
list_nodesYour nodes, each with its state, how many of its checks fail or warn, and the users it carries now.read
get_nodeOne of your nodes, as list_nodes shows it.read
create_nodeRegisters a server as a node and locks the stake from the balance. Returns the install command, which installs the agent in Docker, and the same install as a systemd service. Pass an idempotency_key, and the same one when you retry.manage
update_nodeChanges settings of one node, such as its price per GB, name, description, user limit or visibility. Name only what changes; a setting that nodes do not have is refused and nothing is written.manage
reinstall_nodeMakes a new agent token for a node and the command that installs the agent in Docker with it, and the same install as a systemd service. A node whose runtime is systemd (get_node shows it) takes the systemd one. The agent that is running keeps working on the old token until the new install connects.manage
restart_nodeRestarts the agent on a node's server. Connections through the node drop while it restarts. At most three restarts an hour for a node.manage
delete_nodeDeletes a node and disconnects its agent. Refused while the node has earnings not yet claimed; the refusal says how much.manage
node_checksWhat holds the node back, each check with its fix. With run_now, asks for a new round of outside checks first, at most one every five minutes.read
node_metricsThe node's load, traffic, users and latency over a range: 1h, 6h, 24h, 7d, 30d or 90d.read
node_eventsWhat happened to the node over a range, newest first.read
list_linksThe links at your nodes: which entry sends to which exit, the tunnel and its health. Pass node_id for one node's.read
link_nodesLinks one of your entries to an exit, which may be another operator's if its policy allows.manage
unlink_nodesRemoves a link. Its sessions close and both agents take the tunnel down.manage
list_invitationsThe invitations of a private node, newest first. An invitation's token is shown only when it is made.read
create_invitationMakes an invitation to a private node, with an optional label, use limit and expiry. Its link is shown only in this answer.manage
revoke_invitationRevokes an invitation of a node. With revoke_grants, the access it already handed out goes with it.manage
get_earningsThe account's earnings by day and by role in the route, and its reputation, over 7 or 30 days.read
get_balanceThe balance, the stake locked by nodes, what can be claimed now and whether a claim can be made.read
list_payoutsThe newest 50 payouts with their status, amount and transaction.read

delete_node, unlink_nodes and revoke_invitation are marked destructive in the hints the server sends with them, so a client asks you before it runs one, and the server tells the agent to ask you first. The tools that only read are marked read-only, which many clients run without asking. No tool is marked open-world: none reaches anything outside ProofKit.

What a key cannot do

An agent does what a key does on the Operator API, and no more, whether it signs in or sits on a key. A key is meant to sit in a script's environment, and a leaked one must not reach your money or your account; an assistant you connect is held to the same list. So no key, no connected assistant and no tool can:

  • Touch money. Link or change the wallet, withdraw, or request, prepare or send a payout. get_balance says what can be claimed and whether a claim can be made, and the claim is your own press of Claim on the Wallet page.
  • Change who you are. Change the password, the email address or the second factor, or the Telegram link.
  • Make keys. Make, list or revoke an API key, or connect or disconnect an assistant. Only a signed-in session of the account does that, so a stolen key cannot make another, list its siblings or revoke the key that found it, and an assistant cannot connect another.
  • Read your users. No answer names a user, a user's address or a subscription link.
  • Reach anything else. Support tickets, the admin, or the dashboard's other routes. A key or a sign-in opens this server and, for a key, the Operator API, and nothing else.
  • Approve a node. ProofKit reviews a public node before it takes users, and no tool approves one.
  • Run a node's speed test or switch a link's tunnel. Those stay on the dashboard and the REST API.

Another operator's node, link or invitation answers as one that does not exist, NOT_FOUND, whatever tool asks; the one exception is the exit of a link, which is another operator's to offer (link_nodes) and answers its own policy. A tool returns only ProofKit's words and your own: your nodes' names, descriptions and addresses, your invitations' labels, the reports of your own agents. Where a link has an end that is another operator's, the end is named by a label ProofKit makes (such as "DE exit 05568fa5") and its address is shown only when it is an IP address; the WireGuard key and the SNI of such an exit are not shown. So a stranger cannot put instructions into your agent through the name of a node.

The install command

create_node and reinstall_node return two install commands: install_command, which installs the agent in Docker (the installer adds Docker when it is missing), and install_command_systemd, which installs it as a systemd service instead. A node whose runtime is systemd (get_node shows it) is reinstalled with install_command_systemd: the Docker installer stops, before it changes anything, on a server that already runs the agent as a service. Both carry the node's agent token, so the token passes through the agent's context. Run one of them as root over SSH on the server: an agent in a chat has no SSH, and hands the command to you; an agent that has SSH to the server may run it. Nothing else needs the token. The token is replaced when the agent first connects, so what the agent saw opens nothing after that. Until then treat the command as a secret, and do not paste it into a chat other people can read.

Before you register, server_requirements gives the size a server needs, the oldest kernel and the kernel modules it needs and a command that reads the server and changes nothing. Run that command on the server, before the install or when the install stops: it says what the server lacks, for example a kernel older than 5.6 or one without the module Docker's network needs, and what to do about it. A retry of create_node with the same idempotency_key gives the first answer again for 24 hours, the command included; if the agent has connected since, that command's token is spent, and reinstall_node makes a new one.

Limits

  • Per key and per assistant: 300 requests a minute and 60 writes a minute, counted for the key, or for the assistant's connection, across both of our servers. A key is counted in the count the Operator API uses, so a call through REST and a call through this server are one budget; each connected assistant has a budget of its own. A write is a call of one of the nine manage tools, or of node_checks with run_now. Past the first, the server answers 429 with Retry-After, and every answer shows X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Past the second, a tool comes back as a refusal, RATE_LIMITED, that says how many seconds to wait. A request refused at the door (a wrong key or sign-in, a body that is too large) is not counted.
  • Per node and per operator, the same on the dashboard and the REST API: a node's agent is restarted 3 times an hour; a new round of outside checks is asked for once in 5 minutes for a node, and a second ask inside that time is answered with the latest checks and says so; an operator makes 30 invitations a minute. A refusal says when to ask again.
  • Sizes: a request's body holds at most 256 KiB.
  • Retries: create_node takes an idempotency_key of 1 to 255 visible ASCII characters. A retry with the same key and the same registration is given the first answer and registers nothing, so it locks no second stake. The same key with another registration is refused, IDEMPOTENCY_KEY_REUSED. Pass a fresh key to every registration, and the same one when you retry.

If a call is refused

A tool answers a refusal as a result, not as a protocol error: isError is true, the text reads CODE: sentence, and the same code and sentence follow as {"error": {"code": "…", "message": "…"}} in JSON text, with details when the code carries facts (the amounts of a refused delete, the name of an unknown setting). There is no structuredContent. Say the sentence to the person, and branch on the code. The codes the server gives itself:

  • KEY_REQUIRED (HTTP 401): no Authorization header. The sentence is "Sign in, or send an operator API key as Authorization: Bearer pko_…, made on the Agents page: https://proofkit.org/#/operator/agents". Sign in, or send the key.
  • INVALID_OPERATOR_KEY (HTTP 401): an Authorization: Bearer pko_… that is not a good key: wrong, cut short, expired or revoked, of an account that is not an operator now, or beside a second Authorization header. The sentence is "Invalid, expired or revoked operator API key. Make a new one on the Agents page: https://proofkit.org/#/operator/agents". It is the answer to any request, whatever was asked.
  • INVALID_TOKEN (HTTP 401): any other Authorization that is not a live sign-in of this server: the connection was disconnected or ended (see How long a connection lasts), its token has expired and was not renewed, the account is not an operator now, it is a token of the partner server, or it is no token at all (an empty variable, another scheme, a partner key, the key in capitals or with no Bearer). The sentence is "Your sign-in has ended: sign in again. An operator API key goes in Authorization: Bearer pko_…".

The three 401s carry a WWW-Authenticate header that names the sign-in: Bearer resource_metadata="https://proofkit.org/.well-known/oauth-protected-resource/mcp/operator", scope="read manage", with error="invalid_token" first on INVALID_TOKEN. A client that signs in starts the sign-in from it by itself. The other codes:

  • NOT_FOUND (HTTP 404) with the message "Not found": the server is not open for your account. With a message that names a node, a link or an invitation: it is not yours, or not there.
  • INVALID_BODY (HTTP 400) and PAYLOAD_TOO_LARGE (HTTP 413): a request body that is not JSON, or past 256 KiB. INVALID_BODY is also a tool's refusal when the arguments make a body the operation cannot read, such as an edge_id that is no id.
  • RATE_LIMITED: a budget is spent. It is HTTP 429 with Retry-After for the minute's requests, and a refusal that says how many seconds to wait for the minute's writes and for a node's or an operator's limits.
  • INSUFFICIENT_SCOPE: the key has read and called a tool that changes something. Make a key with manage. An assistant that was allowed read alone gets it as HTTP 403, before anything runs, with WWW-Authenticate: Bearer error="insufficient_scope", scope="read manage" and the same resource_metadata, so that a client can ask you for more: connect it again and leave Manage ticked.
  • INVALID_ARGUMENTS: an argument the tool does not take, one it needs and was not given, or one of the wrong kind; the sentence names the arguments the tool takes. Nothing was written.
  • UNKNOWN_FIELD: update_node was given a setting that nodes do not have; details.field names it, and nothing was written.
  • UNSETTLED_EARNINGS: delete_node was refused because the node has earnings that can be claimed; details.unsettled_nano and details.claim_minimum_nano say how much. Claim on the Wallet page first.
  • IDEMPOTENCY_KEY_REUSED: the idempotency_key of a create_node was used for another registration. INVALID_IDEMPOTENCY_KEY: it is not 1 to 255 visible ASCII characters. IDEMPOTENCY_IN_PROGRESS: the first call with that key is still running; ask again in a few seconds. IDEMPOTENCY_UNAVAILABLE: the store that keeps the answers is out of reach and nothing was done; ask again with the same key.

Any other code is the dashboard's own for the same action, such as INSUFFICIENT_BALANCE when the balance does not cover a stake, NODE_NOT_CONNECTED when a restart finds no agent, or NODE_IS_PUBLIC when an invitation is made to a public node: the sentence says what to do. A tool the server does not have is a JSON-RPC error, "tool not found", instead. The list of every code is in the document https://proofkit.org/operator-api/openapi.json.

Back to contents

03 /API

The Operator API is the REST contract an operator's script or agent calls with a key. It registers a node, reads its checks, changes its settings, links an entry to an exit, and reads the balance and the payouts. It cannot move money, read a user or reach the admin.

Base URL: https://proofkit.org/api/v1/operator

The contract is an OpenAPI 3.1 document, openapi.json, and the API explorer sends calls with your key. Those calls are real: they reach production and your nodes. An assistant that signs in to the MCP server calls the same operations: each of its tools is one of them, with the same rules and limits.

All money is in nanoTON integers: 1 TON = 1,000,000,000 nanoTON. A GB is 1,073,741,824 bytes, with upload and download counted together.

Keys and scopes

Every call except the two public ones carries a key in the Authorization header:

curl -s https://proofkit.org/api/v1/operator/me \
  -H "Authorization: Bearer pko_…"
  • Making a key. A key is made on the Agents page of your dashboard, by a signed-in operator session whose email is confirmed, with its second factor passed when the account has one. It is shown once. A key looks like pko_<8 hex>_<48 hex>, 61 characters.
  • Scopes. A key has the scope read, or read and manage. read reads; manage also changes, and registers, restarts and deletes. Each operation below says which it needs. A key with read alone that calls a change gets 403 INSUFFICIENT_SCOPE.
  • Expiry. A key expires in 30 days, 90 days (the default), a year, or never. An account has at most 10 live keys. Revoke a key on the Agents page: its next call is refused.
  • What a key never does. A key never makes, lists or revokes keys, and never touches money or the account: What a key cannot do lists it. This API reads the money and moves none of it.
  • Whose words. An answer carries your own words and ProofKit's, and none that another account wrote. A link's end that is another operator's has a label of ours for its name (DE exit 05568fa5), and its address only when that is an IP address.

Answers and errors

Every answer is JSON, and every error has one envelope. Dispatch on error.code; the message is for people, and some codes carry details:

{
  "error": {
    "code": "UNSETTLED_EARNINGS",
    "message": "…",
    "details": { "unsettled_nano": 31250000, "claim_minimum_nano": 20000000 }
  }
}
StatusCodeWhen
401INVALID_OPERATOR_KEY, UNAUTHORIZEDINVALID_OPERATOR_KEY: a key that is malformed, unknown, expired or revoked; they are not told apart. UNAUTHORIZED: no key was sent (no Authorization: Bearer pko_…), or a bearer that is not a key.
403INSUFFICIENT_SCOPEThe key may read and the call changes something.
404NOT_FOUNDAn unknown path, another operator's node, link or invitation, or the API not being open to your account. One answer for all of them.
405METHOD_NOT_ALLOWEDThe path exists and does not take that method.
400INVALID_BODY, UNKNOWN_FIELDThe body does not read, or names a setting a node does not have. details.field says which.
409, 422UNSETTLED_EARNINGS, NODE_NOT_CONNECTED, NODE_SUSPENDED, IDEMPOTENCY_KEY_REUSED, …The action is refused for a reason the code names.
429RATE_LIMITEDA budget is spent. Retry-After is the seconds left of the window.
500INTERNAL_ERROROurs.

Another operator's resource answers exactly as one that does not exist, so the API tells nobody which nodes exist. The document lists every code of every operation.

Limits

  • Per key: 300 requests a minute, of which at most 60 may be writes, that is every method but GET and HEAD. They are counted in fixed 60-second windows for both of ProofKit's servers together. X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are on every answer to a counted key.
  • Per node and per operator: a node's agent is restarted 3 times an hour; a new round of outside checks is asked for once in 5 minutes for a node; a node's speed test runs once a minute; an operator makes 30 invitations a minute. A refusal is 429 RATE_LIMITED with Retry-After.
  • Per address: a flood guard in front allows 600 requests a minute from one address. Its 429 carries no X-RateLimit-* headers.
  • Size: a body over 1 MB is refused by the proxy before it reaches the API. Every body the API takes is far smaller.
  • Retries: POST /nodes takes an Idempotency-Key, kept 24 hours for an account, so a retry registers nothing twice. Send a fresh key with every registration, and the same one when you retry.

Revisions

The API has one major version, v1, and it only grows: nothing is removed, renamed, retyped or given another meaning. A breaking change would be /api/v2/operator, announced at least 90 days ahead. Each change to the contract is a revision 1.N, and the document that went out is frozen. Every answer carries the revision in X-Operator-Api-Version, and x-since on an operation is the revision it came in. What changed, and when, is in the Changelog. The rules are the Partner API's.

/me

MethodWhat it doesScope
GETStart here. The account, what it holds, what registering one more node would lock (balance.stake_nano) and whether the balance pays it (balance.covers_stake), and the key that made the call, never the key itself.read

/wanted

MethodWhat it doesScope
GETWhere the network needs nodes: the type, the country and why. Public, so it needs no key, and cacheable for five minutes. It is the list on the operators page.none

/requirements

MethodWhat it doesScope
GETWhat a server needs for a node: the least it may have, what is recommended, the ports the node binds first, the architectures it runs on, the oldest kernel the Docker install runs on (min_kernel, new in 1.1), the kernel modules it needs (kernel_modules, new in 1.1), and preflight, the command that checks a server before you register it. Pass type as exit or entry. Public.none

/earnings

MethodWhat it doesScope
GETWhat the account earned: one entry for each UTC day with earnings, the split by the role your nodes had in the route (alone, entry or exit), and the reputation of your nodes. Pass range as 7d (the default) or 30d. Traffic of your own through your own nodes earns nothing.read

/balance

MethodWhat it doesScope
GETThe balance, the earnings pending and claimable, the stake your nodes lock and what is available after it, and claim: whether a claim can be made now and, when it cannot, the first reason. When claim.can_claim is true, the operator presses Claim on the Wallet page; no key signs or sends a claim.read

/payouts

MethodWhat it doesScope
GETThe newest 50 payouts, newest first, as the Wallet page lists them. A payout never shows the claim that made it.read

/nodes

MethodWhat it doesScope
GETEvery node of yours, a suspended one too. Each has its state, where it is on its way to carrying users, checks, how many of its checks fail and warn, and users_now. The state is judged from the last pass of the checks, at most 5 minutes old.read
POSTRegisters a node and locks its stake from your balance. The answer holds the node's agent token and the commands that install the agent, once: install_command installs it in Docker and install_command_systemd (new in 1.1) as a systemd service. Run one of them on the server as root; a node whose runtime is systemd takes install_command_systemd, because the Docker installer stops on a server that already runs the agent as a service. With an Idempotency-Key, a retry gets the first answer again and registers nothing.manage

A node of yours at the same address that is offline or suspended, and of the same type, is registered again in place and keeps its stake and its earnings. The same key with another body is 422 IDEMPOTENCY_KEY_REUSED, and a request that finds the first one still running is 409 IDEMPOTENCY_IN_PROGRESS.

/nodes/{id}

MethodWhat it doesScope
GETOne node of yours, as the list gives it.read
PATCHChanges the settings the body names and leaves the rest. Every rule is asked before the first write, so a refused change writes nothing.manage
DELETEDeletes the node with what depends on it, closes its sessions and refunds its stake.manage

A setting a node does not have is 400 UNKNOWN_FIELD, with its name in error.details.field. A price changes at most once in 24 hours and rises at most threefold at a time, unless it is 0 now; an exit's price stays within 0.001 and 100 TON. A node whose earnings can be claimed is not deleted: the answer is 409 UNSETTLED_EARNINGS, and you claim on the Wallet page first.

/nodes/{id}/checks

MethodWhat it doesScope
GETThe node's state and each check behind it, with its words and its fix, judged at the read. A suspended node can be read: its checks say why.read

A check's id, subject and status are final. The ids, and what each one means, are in Node checks.

/nodes/{id}/checks/run

MethodWhat it doesScope
POSTAsks for a new round of outside test connections to the node's lines, then read the checks again a minute later. 202, once in 5 minutes for a node.read

It needs only read, because it changes no setting. Where nothing can be asked the answer says why: NODE_NOT_CONNECTED (the node is pending, offline or degraded), NODE_SUSPENDED, or NO_LINES (an entry with no active link to an exit).

/nodes/{id}/metrics

MethodWhat it doesScope
GETThe points the dashboard's charts draw: load, traffic, users and latency, with the capacity, the connection table and the overload levels they are drawn against. Pass range as 1h (the default), 6h, 24h, 7d, 30d or 90d.read

/nodes/{id}/events

MethodWhat it doesScope
GETWhat happened to the node, newest first: agent starts and version changes, overload windows, links going up and down, and olcRTC carrier events. Pass range as for the metrics; a day is the default.read

/nodes/{id}/reinstall

MethodWhat it doesScope
POSTMakes a new agent token for the node and the commands that install the agent with it, in Docker and as a systemd service, with the node's own options. The running agent keeps working on the old token until the new install connects.manage

The token is in the answer this once. A node that is not active is set to pending until the new agent connects.

/nodes/{id}/restart

MethodWhat it doesScope
POSTTells the node's agent to restart. The whole agent restarts, so every connection through the node drops. 202 with sent: true means the command went out, not that the agent has restarted.manage

Only an active or a degraded node: a suspended one is 409 NODE_SUSPENDED, and any other state, or an agent that is not connected, is 409 NODE_NOT_CONNECTED. A node's agent is restarted 3 times an hour.

/nodes/{id}/benchmark

MethodWhat it doesScope
POSTRuns the speed test the dashboard runs and answers with its reading, which is kept on the node. The owner's alone.manage

A node is tested once a minute, the dashboard's tests counted with these. The call waits for the agent for up to 45 seconds. An agent that is away is 400 NODE_OFFLINE, one that cannot fetch the test file whole is 400 BENCHMARK_FAILED, and one that does not answer is 400 BENCHMARK_TIMEOUT.

/links

MethodWhat it doesScope
GETThe links that have an end at a node of yours, with both ends' names and each link's health. Pass node to narrow the list to one of your nodes.read
POSTLinks your entry to an exit, as the dashboard does. The body names edge_id, your entry, and origin_id, the exit, which may be another operator's if its policy allows. tunnel_type is chosen from what both nodes support when left out.manage

/links/{id}

MethodWhat it doesScope
DELETEBreaks a link that has an end at a node of yours, either end. The sessions on it close and both agents take the tunnel down.manage

/links/{id}/tunnel

MethodWhat it doesScope
POSTSwitches a link to another tunnel type. Both nodes must support it. The old tunnel is taken down, the sessions on the link close and the new tunnel is set up.manage

A link that already uses the type answers switched: false and does nothing.

/nodes/{id}/invitations

MethodWhat it doesScope
GETThe invitations of a private node, newest first, with the grants each has handed out. A token is never listed: only its first characters.read
POSTMakes an invitation to one of your private nodes, with an optional label of up to 100 characters, max_uses and expires_at. The token and the link that opens it are in the answer this once.manage

An operator makes 30 invitations a minute.

/nodes/{id}/invitations/{iid}

MethodWhat it doesScope
DELETERevokes an invitation: its link stops working. With revoke_grants=true the access it already handed out goes with it.manage

An invitation that is already revoked, with no grants to revoke, is 400 ALREADY_REVOKED.

Back to contents

04 /Node checks

Every node has a list of checks. ProofKit judges them from what the agent reports and from test connections it makes to the node from outside, over every transport the node offers. You see them on the Checks tab of the node's page, which is the first tab, and in the API as GET /nodes/{id}/checks. A check says what ProofKit saw and, when something is wrong, how to fix it, with the commands to run on the server as root. Check again asks for a new round of test connections, once in 5 minutes for a node.

This guide has a section for each check, under its id. The ids are final: a script can dispatch on them, and the help center links here by id.

  • Failing means something is wrong and the fix is under it. Warning is worth fixing. Pending is no answer yet: no test connection has come back, or ProofKit's own test connections are not reliable at the moment, so nothing is judged. Ok and info need nothing from you.
  • Holding the node back is a separate thing: a few checks keep a public node from being reviewed while they fail. The section of each check says whether it does.
  • Notices. When a failing or warning check has held for 10 minutes (24 hours for the agent's version), ProofKit writes to you once with its fix, by email and in Telegram if you linked it. The alert is called Node checks on the Settings tab of the Wallet page, and Node live is the one that tells you the node carries users.
  • Commands are written for Docker. A Podman node swaps docker for podman, and a node that runs as a systemd service has a command of its own where it differs. The Checks tab shows the ones for your node.

The test connections come from ProofKit's own servers. When a large share of them fail at once, that says ProofKit's prober is unwell and not your node: the checks then read pending, say so, and send no notice.

agent_never_connected

Shown as Agent first connection, for every node. It is pending for 15 minutes after you register the node, failing after, and it holds the node back from review.

What it means. The node is registered, and its agent has not sent a first heartbeat.

Why it matters. Until the agent connects the node carries nobody, and ProofKit cannot test it.

The fix. Run the install command on the server, as root. The dashboard showed it when you registered the node, and Reinstall makes a new one. If you already ran it, the agent's log says why it cannot connect:

docker logs --tail 50 dvpn-agent

On Podman it is podman logs --tail 50 dvpn-agent, and on a systemd node journalctl -u dvpn-agent -n 50 --no-pager.

agent_offline

Shown as Agent connection, for every node. It fails once the last heartbeat is 30 minutes old, and it holds the node back from review while it fails. It sends no notice of its own: the Node offline alert says it.

What it means. The agent has been silent for 30 minutes or more. The detail says how long.

Why it matters. A node counts as offline after 30 minutes without a heartbeat: the routes on it are closed. After 24 hours without one it is suspended (see suspended).

The fix. Check that the server is up and the agent runs, and read its log. If the agent is not running, restart it. If the server was rebuilt, use Reinstall.

docker logs --tail 50 dvpn-agent
docker restart dvpn-agent

On Podman the commands are podman logs --tail 50 dvpn-agent and podman restart dvpn-agent. On a systemd node they are journalctl -u dvpn-agent -n 50 --no-pager and systemctl restart dvpn-agent.

agent_outdated

Shown as Agent version, for a node that runs in a container. It is a warning, and it holds nothing back. ProofKit tells you only after it has held for 24 hours.

What it means. The agent on the node is older than the version the network serves.

Why it matters. New agents carry fixes and new transports, and a node that does not update goes without them.

The fix. A Docker install normally updates itself within 6 hours, through its Watchtower container. Where that does not happen, update by hand: in the install directory, pull the new image and start the agent on it. The restart cuts the node's connections once.

cd /opt/proofkit-dvpn
docker compose pull
docker compose up -d

On Podman, use podman compose pull and podman compose up -d. An agent that runs as a systemd service updates itself and is not judged here.

disk_low

Shown as Disk space, for every node. It warns when the agent reports under 2 GB free, and fails under 1 GB; a failing disk holds the node back from review. With no reading yet it is pending and holds nothing back, because the agent may only be quiet.

What it means. The agent reports little free disk.

Why it matters. Below 2 GB, pulling a new agent image can fail and leave the node offline.

The fix. Free some space or grow the disk. On a Docker or Podman node the commands remove the images that no container uses and no tag names, such as the agent's old image:

docker image prune -f

On Podman it is podman image prune -f. A systemd node has no images: the command trims the system journal, where the agent's log goes, to 200 MB.

sudo journalctl --vacuum-size=200M

cover_site

Shown as Cover site, for exits only. It is pending until the first handshake and failing when the cover site does not pass, and either holds the node back from review.

What it means. Reality borrows the identity of a real website, the cover site, and needs one that completes TLS 1.3 with X25519 and whose certificate ends in a root that ProofKit trusts. ProofKit makes that handshake itself, from its own servers, when the node's cover site changes and once a day.

Why it matters. Reality connections borrow the cover site's identity. A site that fails the handshake gives users a worse disguise, and the node is held back from review until it passes.

The fix. Choose another large, ordinary site in the node's Settings; Regenerate picks one for you. Changing it restarts Xray on the node.

transport_unreachable

Shown as one check for each line, for exits and entries, titled by the transport and its port: Reality on TCP 443, Hysteria2 on UDP 443 and XHTTP on TCP 8444 for an exit. It holds the node back from review while it fails or is pending.

What it means. A line is one way a client reaches the node: an exit has a line for each transport it offers, and an entry has a line for each exit it links to, over each transport the route carries. An entry's relays use TCP 443, one UDP port in 30000-39999 and one TCP port in 40000-49999 for each link. A line fails when 3 of its newest 4 test connections were refused, timed out, were cut off or came back altered; with fewer than 4, when the newest 2 were. A test connection that reached the exit but met an error from the test server, or did not run, says nothing either way.

Why it matters. A line that does not answer is a line no user can use, and the node is held back from review until each of its lines passes.

The fix. A line that is refused or times out means nothing from outside reaches Xray on its port. A firewall in your hosting panel often drops it: open the port there as well as on the server. Hosting panels often open TCP only, so Hysteria2 needs UDP 443 opened by hand. Another program on port 443, a web server for example, blocks Reality too. The command shows what listens:

ss -tlpn | grep ':443'

For Hysteria2 it is ss -ulpn | grep ':443', and for XHTTP ss -tlpn | grep ':8444'. For an entry's lines, open UDP 30000-39999 and TCP 40000-49999 in the hosting panel's firewall, check that the link to the exit is up, and fix the exit's own checks first if it fails them. The commands for an entry list what listens on every port:

ss -ulpn
ss -tlpn

A line that is cut or altered after it opens has a port that answers, and something on the way spoils the connection: a firewall or filter in front of the server that inspects traffic or, on an entry, a link that loses packets. The agent's log shows Xray's errors.

exit_probe_failing

Shown as Test connections through the exit, for exits. It holds the node back from review while it fails or is pending.

What it means. 3 or more of ProofKit's last 4 test connections through the exit failed.

Why it matters. The exit takes connections but they do not get through, so users who are sent to it get nothing, and the node is held back from review.

The fix. Restart the agent: it brings Xray up again, and WARP where it is on. If it keeps failing after that, update the agent and open a ticket in Support.

docker restart dvpn-agent

On Podman it is podman restart dvpn-agent, and on a systemd node systemctl restart dvpn-agent.

host_tuning

Shown as Host network limits, for every node. It warns when the host took only part of the network settings the agent sets when it starts, or refused them, and it holds nothing back.

What it means. The agent tunes the host's connection table, buffers and backlogs when it starts. A security profile such as AppArmor's docker-default can refuse that, and then the node runs on the kernel's small defaults.

Why it matters. The node then runs on the kernel's small defaults, and a full connection table is what that costs.

The fix. A container node leaves the settings it wanted in config/99-proofkit-sysctl.conf, beside the compose file. On the server, copy it to /etc/sysctl.d/, load it, and restart the agent, which reads the size of the connection table when it starts:

cd /opt/proofkit-dvpn
sudo cp config/99-proofkit-sysctl.conf /etc/sysctl.d/
sudo sysctl --system
docker restart dvpn-agent

On Podman, restart with podman restart dvpn-agent. On a systemd node a setting did not take, for example because a kernel module is not loaded or the kernel lacks the key. Running sudo sysctl --system prints the one that failed. Fix that on the host, then restart the agent with systemctl restart dvpn-agent: it writes its files in /etc/sysctl.d/ again at every start, so editing them does not last.

overloaded

Shown as Load, for every node. It is failing while an overload is open, and it holds nothing back.

What it means. The node passed its limits on memory, load, the connection table, dropped connections or bandwidth, on 3 of its last 5 readings. The node's page says which.

Why it matters. While the overload is open the platform sends the node no new connections, until its load comes down.

The fix. There is no command. A full connection table is the agent's to size (see host_tuning). Memory and connections come from real use, so a bigger server is the fix. Raising the user limit does not help.

entry_unlinked

Shown as Links to exits, for entries. It warns when the entry has no active link to an exit, and a warning holds the entry back from review.

What it means. An entry carries traffic only through a link to an exit, and this one has none.

Why it matters. An entry with no link carries nothing and earns nothing.

The fix. Link the entry to an exit: in My Nodes, open the entry's manage row and pick Link to origin. The exit may be yours or another operator's if its policy allows. The API and the MCP server do the same.

carrier_account_error

Shown as Carrier accounts, for exits. It is there only while one of your olcRTC carrier accounts reads error, and it holds nothing back.

What it means. One of the accounts you connected for olcRTC rooms has an error. Rooms already up keep serving, but no new room is made on that account.

Why it matters. While the account reads error, the exit's olcRTC rooms can only be lost, not replaced.

The fix. Open olcRTC carriers on your dashboard and connect the account again.

price_above_ceiling

Shown as Price ceiling, for an approved node. It is there only while the node is priced above the network's ceiling for automatic routing, and it holds nothing back.

What it means. The node is approved, but its price per GB is above the ceiling. The detail names the ceiling exactly. Users still reach the node when they choose it themselves.

Why it matters. Automatic routing sends users only to nodes at or under the ceiling, so the node gets only the users who pick it by hand.

The fix. Lower the price to the ceiling or less in the node's Settings and the node is routed to automatically. The price can change once in 24 hours.

in_review

Shown as Waiting for review, as an info check. There is nothing to fix.

What it means. ProofKit reviews each new public node before it routes users to it automatically. Until then users reach the node only when they choose it themselves.

Why it matters. A new public node is reviewed before the network routes users to it. The review starts once no check holds the node back, and a private node needs none.

The fix. None. Make sure no check on this page holds the node back, and wait.

paused

Shown as Routing paused, as an info check. There is nothing to fix.

What it means. ProofKit has paused routing to the node. It routes nobody new to it, and routes nobody has used for two hours are closed.

Why it matters. While routing is paused the node gets no new users, and a route nobody has used for two hours is closed.

The fix. None. Routing comes back once the platform approves the node again.

suspended

Shown as Suspended, for every node. The node is offered to nobody.

What it means. A node is suspended after 24 hours without a heartbeat, or when its health score falls under 30; an admin can also suspend one. A reconnecting agent does not lift it.

Why it matters. A suspended node is offered to nobody, so it carries no users and earns nothing.

The fix. Register the node again from the same server, with the same type, or Reinstall it. Either keeps its ID and stake (Reinstall keeps its settings and price too) and gives a new install command: run it on the server, as root, and the new agent's first connection makes the node active. A health score still under 30 suspends it again. If you do not know why it was suspended, open a ticket in Support.

Back to contents

05 /Payouts

How a node earns, how the earnings reach your wallet, and how the stake and the rest of your deposit come back. Everything here happens on the Wallet page of your dashboard, and your wallet approves every transaction. A key or an agent can read the numbers and can move none of the money: see What a script or an agent can see.

What you earn

You set a price per GB for each node: at least 0.001 TON for an exit, at most 100 TON, and an entry may be free. Users see your price with the 20% network fee added, and you receive 80% of what they are charged, which is 0.96 times your price for every GB, upload and download both counted. A GB is 1,073,741,824 bytes.

Your priceUser pays / GBYou get / GBYou get / 100 GB
0.001 TON0.0012 TON0.00096 TON0.096 TON
0.01 TON0.012 TON0.0096 TON0.96 TON
  • Two hops. On a route with an entry and an exit, the charge is shared between the two nodes by their prices, and each of the two operators receives 0.96 times its own price per GB. An entry priced at 0 sells nothing by itself, and its exit receives the whole share.
  • Your own traffic earns nothing. Traffic of your own through your own nodes is not counted.
  • olcRTC is the exception. From 20 November 2026 its traffic is charged at one network-wide price per GB, and you receive 80% of that price for the olcRTC traffic your node carries.
  • Do not plan around a fixed income. How much traffic a node gets depends on its country, price, uptime and demand, and the network is young.

The price is fixed into each session when the user connects, so a change applies to new sessions only. You can change a price once every 24 hours, and an increase can be at most three times the current price.

Pending and claimable

Every traffic report from your nodes adds your share of the charge to pending. Nothing is paid out automatically: earnings are paid on-chain, out of the users' deposits in the escrow contract, when you claim them.

  • Pending is your share of every charge that has not been paid out yet.
  • Claimable is the part that can go into a claim now. It comes from users who deposited from a TON wallet and owe you at least 0.001 TON each. The rest stays pending until it qualifies.
  • The minimum is 0.02 TON claimable in all. Below it, Claim earnings does not appear, and the earnings stay pending and add up.

Claiming

  1. Press Claim earnings under Wallet → Payouts. ProofKit signs one batch claim covering up to 50 of the users you are owed by, the largest amounts first. The signature is valid for 5 minutes.
  2. Your wallet opens with the claim and 0.06 TON attached for gas. Approve it.
  3. The escrow contract checks the signature, takes each user's amount from their deposit, sends 80% of the total to the wallet linked to your operator account and keeps 20% as the platform fee. Your 80% is the pending amount you claimed.

Your wallet needs at least 0.08 TON for the gas, and the dashboard checks before it opens the wallet. The contract sends back whatever part of the 0.06 TON the claim does not use, in a separate message marked ProofKit gas refund, so a claim costs a few thousandths of a TON plus your wallet's own fee.

  • Who a claim leaves out. The contract rejects a whole claim if one user's deposit cannot cover their amount, so ProofKit reads each deposit from the chain before it signs, and leaves out a user whose deposit no longer covers what they owe. Their earnings stay pending, as do those of users beyond the first 50. Claim again once this claim is confirmed.
  • If you do not approve in time. A signed claim expires 5 minutes after it is made, and nothing is lost. A few minutes after it expires, once the escrow contract shows it was never used, Claim earnings makes a fresh claim from the same earnings. Until then the page says that your previous claim is still being confirmed.
  • Statuses. In the settlements table a claim is pending when it is signed and not on-chain yet, sent when your wallet sent it, and confirmed when the payout transaction is found on-chain. A claim that a newer one replaced is voided and hidden.

The stake

Registering a node locks 1 TON of your balance as its stake, and registration fails if less than 1 TON is available. The stake makes cheating cost money and makes running many fake nodes expensive. It is not spent: the deposit stays yours, and the locked part is not withdrawable while the node exists.

  • Slashing. ProofKit's checks raise flags on a node, warnings and critical ones. When a node has 3 or more critical flags within 24 hours, 10% of its current stake is slashed, at most once in any 24 hours. A slash only reduces the stake: the node keeps running. If you think a slash was a mistake, open a ticket in Support under Slash appeal. It is reviewed by hand, and nothing is refunded automatically.
  • Suspension is separate. A node is suspended when it has been offline for 24 hours or when its health score falls under 30. Downtime is never slashed. suspended says how to bring a node back.
  • Getting the stake back. Delete the node. The stake, less anything slashed, is released to your balance at once, and you can withdraw it. A node can be deleted only once its claimable earnings, 0.02 TON or more, have been claimed.

Withdrawing

A withdrawal returns your own deposit to your wallet. Earnings are paid separately, with Claim earnings.

  1. On the Funds tab of the Wallet page, choose Withdraw, enter an amount from 0.05 TON and press Sign with wallet. The dashboard asks ProofKit to co-sign the withdrawal. The co-signature is valid for 5 minutes.
  2. Your wallet opens with the withdrawal and 0.05 TON attached for gas. Approve it: the contract checks the co-signature and sends the amount to your wallet.

You can take out the smaller of two numbers: your balance less the stake your nodes lock, and your deposit in the contract less what is still pending against it (your own unclaimed charges, claims on their way and withdrawals approved a moment ago). Of the 0.05 TON for gas the contract keeps a few thousandths and sends the rest back. The wallet needs at least 0.08 TON before the dashboard goes ahead.

The deposit is not held by ProofKit: it sits in the escrow contract under your wallet's address, and everyday withdrawals need ProofKit's co-signature, which the dashboard gets for you. If the contract has been paused for 30 days, every depositor can withdraw without it.

The wallet

An account links one TON wallet, with TonConnect, and a wallet can be linked to one account only. You can link another wallet at any time. What the previous wallet holds in the escrow stays on your balance and pays for traffic first; to withdraw it, link that wallet again.

What a script or an agent can see

GET /balance in the API gives the balance, the pending and claimable earnings, the stake your nodes lock and claim: whether a claim can be made now and, when it cannot, the first reason, one of WALLET_NOT_LINKED, CLAIM_NEEDS_REVIEW, CLAIM_SETTLING and BELOW_MINIMUM. /payouts lists the newest 50 payouts and /earnings the earnings by day. The MCP server has a tool for each of them. When claim.can_claim is true, an agent tells you to press Claim: no key and no agent can claim, withdraw, link a wallet or change one.

Back to contents

06 /Changes

What was added to the Operator API, and when. The API has one major version, v1, and it only grows: nothing is removed, renamed, retyped or given another meaning, and a breaking change would be /api/v2/operator, announced at least 90 days ahead. A revision is listed on the day it went live. Its document is frozen at /operator-api/versions/1.N.json, with a checksum in /operator-api/versions/index.json, and a published revision is never edited. The rules are the Partner API's.

1.1 (2026-10-30)

  • POST /nodes and POST /nodes/{id}/reinstall answer with two install commands. install_command installs the agent in Docker, the way the dashboard shows it, and the new install_command_systemd installs it as a systemd service. In 1.0 install_command was the systemd install: a script that ran it now gets the Docker install, which sets Docker up itself when it is missing, and the old command is install_command_systemd. Both hold the agent token. This gives a field another meaning, which the API does not otherwise do: it was done once, here, while the API answered only ProofKit's own account and no other operator's script had read the old value. From now on nothing is given another meaning. A node whose runtime is systemd (GET /nodes/{id} shows it) is reinstalled with install_command_systemd: the Docker installer stops, before it changes anything, on a server that already runs the agent as a systemd service.
  • GET /requirements names, in the new kernel_modules, the kernel modules each way of installing needs: xt_addrtype for Docker, none for a systemd service. It names, in the new min_kernel, the oldest Linux kernel the Docker install runs on, 5.6. The installer's check (install.sh --check) asks for both in the Docker install, which is its default, and says how to fix what is missing; --mode=systemd leaves them out. In the check's --json report the two questions are the entries of checks with the ids kernel_version and docker_netfilter.

1.0 (2026-10-28)

The first revision: keys, and the contract at /api/v1/operator that a key opens.

  • Keys: pko_<8 hex>_<48 hex>, sent as Authorization: Bearer, with the scope read or read and manage. A signed-in operator session makes, lists and revokes them; a key never does, and the key routes are the dashboard's, not part of this document. Every answer of the contract carries X-Operator-Api-Version: 1.0. A key has 300 requests a minute, of which 60 may be writes, with X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Every 429 the API gives carries Retry-After, the seconds that are left of the window that refused the call: a key's budgets, and the limits of a node or an operator (a restart, a benchmark, Check again, an invitation).
  • An unknown path is 404 NOT_FOUND and a wrong method 405 METHOD_NOT_ALLOWED, in the one error envelope; a 500 is INTERNAL_ERROR. Another operator's node, link or invitation answers as one that does not exist.
  • POST /nodes takes an Idempotency-Key, kept 24 hours per account. A retry with the same key and body is given the first answer again, the agent token included, with Idempotent-Replayed: true.
  • The account: GET /me.
  • Public, with no key: GET /wanted and GET /requirements.
  • The money, read only: GET /earnings, GET /balance and GET /payouts. No key requests, signs or sends a claim or a withdrawal. GET /balance says, from one reading of what is claimable, whether the Wallet page can make a claim now. CLAIM_SETTLING means the page would refuse a claim without asking the chain (inside the 5 minutes after an earlier claim's window, or while a payout is being booked for it): a claim that was never sent and has run out is not that, because pressing Claim replaces it.
  • Nodes: GET /nodes, POST /nodes, GET /nodes/{id}, PATCH /nodes/{id}, DELETE /nodes/{id} and POST /nodes/{id}/reinstall. A change names only what it changes, and a setting a node does not have is 400 UNKNOWN_FIELD with details.field. A delete that would strand claimable earnings is 409 UNSETTLED_EARNINGS with details.unsettled_nano and details.claim_minimum_nano (the claim.min_payout_nano of GET /balance), and leaves the agent connected.
  • Checks, metrics and events: GET /nodes/{id}/checks, POST /nodes/{id}/checks/run, GET /nodes/{id}/metrics and GET /nodes/{id}/events. A check's since is when it took the status it has; a turn between fail and warn is one spell, so since keeps the spell's start and any other change of status starts again from the pass.
  • The agent: POST /nodes/{id}/restart for a node that is active or degraded (409 NODE_SUSPENDED for a suspended one, 409 NODE_NOT_CONNECTED for any other status or an agent that is not connected), 3 times an hour for a node (a restart whose command could not be sent is not counted), and POST /nodes/{id}/benchmark, the owner's alone.
  • Links: GET /links, POST /links, DELETE /links/{id} and POST /links/{id}/tunnel. No answer of the API says a word another account wrote. A listed link names an end that is another operator's by our own label (DE exit 05568fa5, RU entry 05568fa5), never by the name its owner wrote, and gives its address (edge_endpoint_host, origin_endpoint_host) only when that is an IP address, IPv4 or IPv6: any other text is null. A link made to another operator's exit leaves out that exit's WireGuard key (wg_origin_pubkey is null), and a clash of SNIs names the SNI only when the exit is the caller's. The caller's own nodes keep their names, addresses and keys.
  • Invitations to private nodes: GET /nodes/{id}/invitations, POST /nodes/{id}/invitations and DELETE /nodes/{id}/invitations/{iid}.

Back to contents

proofkit.

A pay-per-GB VPN on TON. Several transports, one of them shaped like a video call; an open network where anyone can run a node; a non-custodial escrow you withdraw from.

Sign in · Help center

Product

  • Marketplace
  • Network status
  • Proofs explorer
  • Speedtest
  • Pay per GB
  • Paying with TON
  • Partner API

Use cases

  • Anti-censorship VPN
  • No-logs VPN
  • Private VPN node
  • VPN for Russia
  • VPN for Iran
  • VPN for China

Protocols & apps

  • VLESS Reality
  • Hysteria2
  • XHTTP
  • olcRTC (video call)
  • Protocols compared
  • Ghostlane setup
  • Hiddify setup
  • V2RayNG setup

Run a node

  • For operators
  • Operator docs
  • Operator API
  • Become an operator
  • Operator guide
  • Self-hosted VPN
  • How the network works
  • Telegram bot

Legal

  • Privacy
  • Terms
  • Acceptable use
  • Refunds
  • Cookies
  • Imprint & contact
  • Cookie settings
© 2026 ProofKit · An open network: anyone can run a node. Ghostlane, an open-source client: App Store · Google Play · GitHub Reality · Hysteria2 · XHTTP · olcRTC