Selling a GPU
One host, two processes, and a payout address you already hold. A provider is not a validator: it posts no stake, joins no validator set, and needs no attestor key. It is a peer that sells work.
Run the preflight first
Start here, not at the end. Genesis is applied once and the fact is recorded, so a node started against the wrong one cannot be corrected in place - the store has to be deleted and the node started again. Every other mistake on this page is cheap before the first start and expensive after it.
# from a checkout of matrix-os, on the GPU box itself
scripts/gpu-provider-preflight.sh /etc/matrix/gpu-provider.yamlIt reads the config the way the node reads it, probes the model server for a real completion, checks what this box is listening on and whether it can reach its bootstrap peers, and hands the config to the node's own production preflight rather than keeping a second copy of those rules. It writes nothing and starts nothing.
== the config, read as the node reads it
ok genesis: 4 allocations
ok consensus.validators: 3 entries
ok consensus.participate_in_open_set: false, a peer that sells
FAIL admin.addr: 0.0.0.0:9090 is not loopback. The admin API's key can move
funds; the only thing between it and the internet would be a firewall
rule. Bind 127.0.0.1 and reach it over an SSH tunnel.
FAIL inference.echo_provider: set to "demo-inference-provider". That is a
GPU-free stub answering paying prompts. Clear it.
== the model server
ok eth:0x8f2c...: /health answers 200
ok eth:0x8f2c...: answered as "llama-3.3-70b", reporting 9 + 3 tokens
NOT READY: 2 failed, 3 to decide.Failures are mistakes. Warnings are decisions - selling at cost, validating as well as selling, reselling a vendor's API instead of your own weights - and they are listed so you make them deliberately rather than inherit them from a generated file.
What you are selling
One unit is one token. The prompt and completion counts the model server reports are summed and multiplied by your per-unit price, and there is no separate per-request or per-second charge. Those counts are what settles, which is why the preflight sends one real prompt and reads the usage object back: a server that reports nothing leaves the node deriving the number itself, from an approximation of somebody else's tokeniser.
A reasoning model bills for its working as well as its answer, and it is delivered with the completion for that reason. The receipt commits to both, so a seller cannot charge for one body of reasoning and hand over another.
Provider emission at launch is 0 per block. Settlement is the whole of your revenue, so the price you set is the whole of it.
1. The model server, on loopback
export MATRIX_VLLM_API_KEY="$(openssl rand -hex 32)"
vllm serve <model-id> \
--host 127.0.0.1 \
--port 8000 \
--api-key "$MATRIX_VLLM_API_KEY" \
--served-model-name <the-name-you-will-advertise> \
--max-model-len <fits-in-vram> \
--max-num-seqs <concurrent-sequences> \
--gpu-memory-utilization 0.90Bind it to 127.0.0.1 and nothing else. It has no authentication worth exposing and no metering; the node in front of it is what authenticates, meters and charges. A model server on a public interface is free inference for whoever finds the port, and the preflight fails on it.
--api-key is not optional even on loopback. The openai backend fails construction on an empty key, so a keyless server means the node refuses to start rather than advertising capacity it cannot reach. That failure is the good outcome.
2. Join the network you are selling on
This is the step that looks optional and is not. A provider does not validate, but it is a full node: it replays the chain from height zero and checks each block's state root against its own ledger. A node that starts with an empty or invented genesis computes a different root at height zero and refuses every block the network sends it. What you see is a node that connects to its peers and never advances.
# on a node already in the network, copy both sections verbatim:
awk '/^[a-z_]+:/{sec=$1} sec=="consensus:"||sec=="genesis:"' /etc/matrix/config.yamlThe validator list has to be the genesis set, not the set validating today. Each replayed block is checked against the set as it stood at that height, so a node handed the current set rejects early history and never catches up. Neither section holds a secret: validator ids are public keys and genesis allocations are public chain data.
3. Declare the backend
inference:
# A freshly initialized node sets this to a GPU-free stub. Clear it.
echo_provider: ""
backends:
# The id IS the payout account. Use the wallet address you already hold.
- id: "eth:0x<your-wallet-address-lowercase>"
kind: openai
base_url: "http://127.0.0.1:8000"
api_key_env: MATRIX_VLLM_API_KEY
request_timeout: 10m
health_check_path: /health
health_check_interval: 30s
models:
- <the-name-you-will-advertise>
capacity: 200000
price_per_unit: 4
quote_ttl: 24hThe id is the account revenue is paid into. Settlement credits it directly and nothing later asks whether anyone holds its key, so an invented name is a listing that earns money nobody can spend. Use the address you already have in a wallet.
request_timeout defaults to 60 seconds, which is a fair cap on somebody else's hosted API and the wrong one on a GPU you own. A local model asked for a few thousand tokens routinely runs longer, and the fixed cap fails the job after the GPU has produced the answer: electricity spent, nothing sold.
health_check_path should point at /health, which reports on the inference engine. The default /v1/models can still answer from a list built at startup while the engine is wedged. A failed probe suspends the listing on the first failure, because being off the market for one interval costs a few routing decisions while staying on it costs a buyer a reservation, a wait, and a failed request.
4. Expose exactly what buyers need
9000 libp2p P2P open to your bootstrap peers and the network
9090 admin API never. Loopback only; its key can move funds
9093 Connect HTTP the buyer door. Behind TLS
8000 the model server loopback only, alwaysTerminate TLS in front of 9093. The Connect endpoint speaks plain HTTP and a buyer's API key travels in an Authorization header, so without TLS every buyer credential crosses the network in the clear.
Turn on connect.signed_writes if you want to sell to buyers who are not you. Without it, buyer is a bare string and only accounts whose keys your node custodies can pay; with it, the buyer signs an authorization bound to one provider, one prompt, and one moment.
5. Buy from yourself once
A completion that returns and a job that settles are different facts. Prove both before telling anyone the endpoint exists.
curl -s https://<your-host>/v1/chat/completions \
-H "Authorization: Bearer <a matrix api key whose account is funded>" \
-H 'Content-Type: application/json' \
-d '{"model":"<the-name-you-will-advertise>","messages":[{"role":"user","content":"hi"}]}'
# then confirm the money actually moved, which is a different fact
matrix --api-key <key> balance --account eth:0x<your-wallet-address>An API key with no account field can drive every other surface and cannot buy inference. That is the safe reading, and it is also the first thing to check when this returns 401 with a key that works everywhere else.
Two things to tell your buyers
Your payout address is public. It is your order-book id, so every buyer and every peer that receives an announcement sees it, and so does anyone reading the chain. Worth knowing before you use an address that is also your personal wallet.
The provider sees the prompt. The model runs on your hardware, so the plaintext passes through it. There is no confidential-compute claim here. Say so rather than letting buyers assume otherwise.
Next
- The full runbook - sizing the model to the GPU, pricing from a measured cost basis, and the operating notes
- Compute marketplace - how a quote becomes a reservation and a reservation becomes a settlement
- Network setup - what a node needs to agree with its peers at all