Plug an agent into the pond.
The whole GPU spot market — 2,500+ live offers from a dozen provider feeds — behind one MCP endpoint and one JSON API. Reading it needs no key, no signup, no scraping: an agent can price the market, watch for a number, and pick the cheapest machine in one call. Renting has honest boundaries: execution runs on your provider key, or on a resting limit order your own keyholder daemon fills — your key rides one pass-through call and is never stored, and we hold no custody.
MCP, natively.
The endpoint speaks MCP streamable HTTP, stateless, at https://compute.pangle.online/mcp/.
| tool | what it answers |
|---|---|
| search_offers | ranked live offers — gpu, country (ISO-2, comma list, or EU), region, offer_class, max_price_per_gpu_hr, min_vram_gb, min_gpu_count (8 = full nodes), limit |
| get_offer | one offer by id |
| market_stats | totals, idle share, per-provider freshness and health |
| price_history | the tape for one GPU: per-provider min/median per GPU-hour over time |
| idle_history | the idle-capacity index series |
| best_value | perf-per-dollar — FP16 spec TFLOPS per dollar-hour at each model's best live price (spec-sheet basis, disclosed; unrated models named) |
| provider_reliability | how dependable each provider's feed has been (poll success), plus their own machine-reliability figure where reported |
| price_position | rent-now-or-wait context: today's best price as a percentile of its own trailing 7/30-day range — descriptive, never a forecast |
| spot_spread | the live on-demand vs interruptible discount per model; pass a model for its history |
| will_it_fit | will this model fit, and what does a fitting machine cost right now — VRAM arithmetic (weights + KV cache + stated overhead, working shown) against the live book, cheapest fitting offers first; pass a model slug or params_b, precision fp16/int8/int4, context, batch. Pass YOUR measured tokens_per_s and each offer gains usd_per_m_tokens — we never invent a throughput number |
| true_cost_notes | the billing fine print behind the $/hr, per provider, with a source URL and our verification date on every claim — per-second vs per-minute billing, storage billed while stopped, bandwidth/egress, the commonly-missed gotchas; providers we have not verified carry no claims. No arguments |
| create_watch | arm a price tripwire; fires under your line, re-arms 2% above it |
| watch_status | a watch's state, recent events, best matching offer now |
| delete_watch | disarm and remove a watch |
| create_limit_order | rest a standing "rent when the price is at or under my line" order (gpu, max_price_per_gpu_hr); returns the order with order_secret shown once. Triggers are checked on the poll cadence, about every five minutes; add standing:true and it refills after a preemption |
| limit_order_status | an order's state (armed / ticketed / filled / cancelled / expired), last price seen, recent events; pass order_secret to also read the live fill ticket when one is cut |
| cancel_limit_order | cancel an order — needs its order_secret. Stops supervision; it cannot destroy a running machine, because the station holds no key |
| rent_offer | place a rental on YOUR provider key (Vast or RunPod) — dry-run by default, live only with confirm=true; the key passes through one call and is never stored |
| rent_best | one-shot: search the live book and place on the best match ("cheapest H100 under $2/hr, go"); walks past at most 3 already-gone offers, price cap absolute |
| rental_status | live state, uptime and estimated spend of an instance you placed; shows the budget guard if armed |
| destroy_rental | tear down an instance you placed, same pass-through key |
| fill_limit_order | execute a limit order's live fill ticket on YOUR provider key (order_id, order_secret, api_key) — only while the order is ticketed, re-quoted live and refused above the order's line, idempotent per order so a retry returns the first receipt rather than a second machine |
| register_account | optional metering account — bearer token shown once; pass as account_token on rent calls to build a ledger |
| account_usage | your metered ledger: placements, hours, estimated USD, platform fee (2.5%, $0.01 min, $5/lease/day cap) |
Claude Code — one line:
claude mcp add --transport http compute-wick https://compute.pangle.online/mcp/
Claude Desktop — recent versions connect to remote servers directly; add under mcpServers in claude_desktop_config.json. Older builds can bridge with mcp-remote:
{ "mcpServers": {
"compute-wick": { "url": "https://compute.pangle.online/mcp/" }
} }
// or, over stdio:
{ "mcpServers": {
"compute-wick": { "command": "npx",
"args": ["mcp-remote", "https://compute.pangle.online/mcp/"] }
} }
Python (the mcp SDK):
from mcp import ClientSession from mcp.client.streamable_http import streamable_http_client async with streamable_http_client("https://compute.pangle.online/mcp/") as (r, w): async with ClientSession(r, w) as session: await session.initialize() offers = await session.call_tool("search_offers", {"gpu": "H100", "limit": 5}) # mcp SDK 1.x: streamablehttp_client, unpacks (r, w, _)
LangChain / LangGraph (via langchain-mcp-adapters):
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"compute": {"transport": "streamable_http",
"url": "https://compute.pangle.online/mcp/"}})
tools = await client.get_tools() # hand these to any agent
Plain JSON, if you'd rather.
Every MCP tool has a REST twin. No auth on any of them.
| endpoint | returns |
|---|---|
| GET /api/fit | will-it-fit: deterministic VRAM arithmetic against the spec table, working shown and assumptions listed (MCP twin: will_it_fit) |
| GET /api/costnotes | the billing fine print nobody reads, verified per provider with its source (MCP twin: true_cost_notes) |
| GET /api/specs | the workload-lens spec table: dense FP16 tensor TFLOPS per model — spec-sheet ceilings, not benchmarks; absent model = unrated |
| GET /api/offers | ranked offers; filters gpu, country (ISO-2, comma list, or EU), region, offer_class, provider, max_price, min_vram, min_gpu_count, per_model (1 = each model’s cheapest row), limit |
| GET /api/gpus | GPU models with counts, floors, idle tallies |
| GET /api/stats | market snapshot and provider health |
| GET /api/history | price tape per GPU (gpu=, offer_class=, hours=) |
| GET /api/idle-history | idle-capacity index series (hours=) |
| GET /api/value | perf-per-dollar board (offer_class=, min_vram=, limit=) |
| GET /api/reliability | provider feed dependability over the trailing window (days=) |
| GET /api/timing | price percentile vs trailing range for one model (gpu=, offer_class=) |
| GET /api/spread | on-demand vs interruptible discount per model (gpu= adds history) |
| POST /api/watches | arm a tripwire; returns private feed + status URLs |
| POST /api/rentals | rent an offer with your own provider key; dry_run=true by default returns the exact call it would make |
| POST /api/rentals/best | one-shot search+place with the same body plus search filters |
| POST /api/rentals/{provider}/{id}/status | live state + estimated spend (body: your api_key — keys never ride GET) |
| DELETE /api/rentals/{provider}/{id} | destroy an instance you placed (body: your api_key) |
| POST /api/account | optional metering account; GET /api/account/usage (Authorization: Bearer) reads the ledger |
curl 'https://compute.pangle.online/api/offers?gpu=4090&max_price=0.30'
curl -X POST https://compute.pangle.online/api/watches \
-H 'Content-Type: application/json' \
-d '{"gpu": "H100", "max_price_per_gpu_hr": 1.50,
"offer_class": "interruptible",
"webhook_url": "https://your-agent.example/hook"}'
When a watch trips, your webhook receives {watch_id, gpu, line, best, ts} where best is the full offer that crossed your line — including the link to rent it. Webhooks must resolve to public address space; watches re-arm when the price climbs 2% back over the line. Watches expire 30 days after creation (expires_at on every watch response) — re-create one to keep it standing.
Renting through the broker. Bring your own Vast or RunPod API key. Every placement is re-quoted live with your key before money moves, refused if the live price exceeds your max_price_per_gpu_hr, and guarded by an optional idempotency_key so a retried request returns the first receipt instead of a second machine. Dry-run is the default; a live placement requires confirm: true.
The budget cap. Pass auto_destroy_budget_usd and this station watches your rental every five minutes and destroys it when estimated spend (price × uptime, conservative) reaches the cap — the safety net for unattended agents. Plain about the mechanics: arming the cap keeps your key in process memory only, never disk; if the station restarts, the guard is gone while your rental keeps running, and every receipt that arms one says exactly that. Poll rental_status as your own backstop.
Limit orders & the keyholder.
A watch tells you the price crossed your line. A limit order goes further: the station cuts a signed fill ticket naming the exact offer, and whoever holds your provider key executes it. The station never holds the key — that is the whole design.
curl -X POST https://compute.pangle.online/api/orders \
-H 'Content-Type: application/json' \
-d '{"gpu": "RTX 4090", "max_price_per_gpu_hr": 0.02,
"offer_class": "interruptible",
"auto_destroy_budget_usd": 1.0}'
The response carries order_secret — shown once, it authenticates ticket reads and fills for this order only, and can rent nothing by itself. Three ways to execute when the ticket is cut:
1. The keyholder sidecar (recommended): a 406-line open-source stdlib-Python daemon you run on your own machine — it long-polls the order, verifies the ticket's HMAC signature, and fires the fill with your key from your side. Every safety property is readable in one sitting (the whole station is open source: github.com/aitools420/compute-wick, AGPL), and it dry-runs by default:
# fetch it, read it, run it — stdlib only, no installs curl -sO https://compute.pangle.online/sidecar/keyholder.py ORDER_ID=<id> ORDER_SECRET=<secret> PROVIDER_KEY=<your vast/runpod key> \ python3 keyholder.py # dry-runs until you add CONFIRM=1
A real pass, replayed. The loop above, run against the live book on 2026‑08‑24 (dry-run; the order's line sat above the market, so the ticket cut on the next check). The platform_fee block postdates that run and is reproduced from a current receipt:
keyholder: watching order 7dd9e5ee… (dry-run only — set CONFIRM=1) keyholder: ticket — RTX 4090 on vast at $0.1215/GPU/hr (line $0.2000) keyholder: receipt: { "dry_run": true, "would_place": { "method": "PUT", "url": "https://console.vast.ai/api/v0/asks/46655716/", … }, "live_price_final_per_gpu_hr": 0.121481, "fee_bps": 0, "platform_fee": { "bps": 250, "min_usd": 0.01, "cap_usd_per_lease_day": 5.0, "charged_on": "what we settle, never on the provider's price" }, "note": "no rental was placed; repeat with dry_run=false and confirm=true to execute" } keyholder: dry-run complete (order stays armed); set CONFIRM=1 for real fills.
2. Your own agent: long-poll POST /api/orders/{id}/ticket ({order_secret, wait: 55}), then POST /api/orders/{id}/fill with {order_secret, api_key, confirm: true}. MCP tools create_limit_order / limit_order_status / cancel_limit_order / fill_limit_order wrap the same rail.
3. A human with curl, if you like your fills artisanal.
Plain about the mechanics: triggers are checked on the station's poll cadence (every ~5 minutes against the keyed feeds; keyless catalogs refresh half-hourly — not tick-by-tick); a ticket lives ~4 minutes then the order re-arms; fills re-quote the live price and refuse above your line; the fill is idempotent per order, so a retry can never rent a second machine; orders expire after 30 days.
Standing orders — compute that survives.
Interruptible machines die; that is why they are cheap. A standing order is a limit order that refuses to: add "standing": true and after a live fill the keyholder keeps watching the machine, reporting what it sees every ~45 seconds. Two consecutive dead observations and the order re-arms — the station cuts a fresh ticket against the current best offer on the book, which may be a different host or a different provider entirely. No single provider can move you off itself; a layer that sees the whole market can. Every death, gap and refill lands on the order's event chain, receipts included.
curl -X POST https://compute.pangle.online/api/orders \
-H 'Content-Type: application/json' \
-d '{"gpu": "RTX 4090", "max_price_per_gpu_hr": 0.25,
"offer_class": "interruptible", "standing": true,
"max_refills": 5, "cooloff_seconds": 120,
"auto_destroy_budget_usd": 5.0}'
Plain about the limits, and why each one exists:
We re-provision the machine, not the work. GPU memory cannot be checkpointed portably across hosts, so resume comes from your own checkpoints: write them to your bucket every few minutes, restore on boot (CHECKPOINT_URL rides every refill). Right tool for checkpointable training and stateless inference; wrong tool for sticky in-memory state.
Refills need your sidecar alive. It is both the witness and the executor — if it is down the station cannot see the death and could not place the replacement anyway, so silence gets you a heartbeats-lost webhook, never a guess. Run it under docker --restart=always or systemd.
Death is confirmed, not assumed. Two consecutive dead observations (~90 seconds), then a cooloff before the refill so a flapping host cannot spend you in a loop. Detection is minutes, not milliseconds. Every refill re-quotes live and holds your original line — if nothing on the book is at or under it, the order says starved and watches on.
Cancelling stops supervision, not the machine. The station holds no key and cannot destroy anything; end a live machine yourself or let its auto_destroy_budget_usd guard do it.
The zombie watchdog. Forgotten machines are how this industry quietly profits; a standing order can refuse that too. Add spend_alert_usd or age_alert_hours and the station webhooks you once per machine when it outlives your line — "up 6.2h ≈ $4.23 — still yours?" The number is an estimate (uptime × price; the provider's bill is authoritative, and the alert says so), alerts fire only while heartbeats flow, and nothing is ever killed on an inference — auto_destroy_budget_usd, a cap you set, remains the only hard stop.
The probe & the trust tape.
The oldest lie in this market is the gap between advertised and delivered: the throttled card, the gimped PCIe slot, the "dedicated" bandwidth that isn't. The probe is a one-minute, stdlib-only script you run on the machine you rented — it reads what the box actually is (GPU identity, VRAM, PCIe link, power limit, disk, network), prints your receipt, and posts the reading to the public trust tape:
curl -sL https://compute.pangle.online/sidecar/probe.py | ADVERTISED="RTX 4090" python3 -
Plain about how the tape earns belief, and what it costs you (nothing):
We never rent the fleet; you already did. Readings come from renters on machines they are paying for anyway — the reading costs you sixty seconds and answers "did I get what I paid for" on the spot. NO_REPORT=1 keeps your receipt private; the tape survives on the renters who don't mind.
Tiers of belief, labeled. A reading signed with a live order's secret (ORDER_ID/ORDER_SECRET) counts as verified — the submitter provably holds an order on this book, and faking that at scale means renting at scale. Anything else is anonymous and says so. No aggregate wears the verified badge under 5 verified readings — a young tape reads accruing, never dressed up. GET /api/trust.
What it can't see. The probe reads hardware facts, not your workload, keys or data — the source is the one file you just read. Coverage follows liquidity: heavily-rented hosts get measured; the long tail shows no readings rather than a guessed score.
The badge.
A live SVG anyone can hotlink, refreshed every five minutes, straight off the book. The default is the cheapest listing for a model and it says from — a floor, not a price. Add &stat=index for the per-chip on-demand median, the number /chips/ publishes and a stranger can recompute. Paste it in a README, a blog, a dashboard:
# markdown   # any model works: ?gpu=4090, ?gpu=A100 · add &offer_class=interruptible for the idle floor · &stat=index on H100 SXM / H100 PCIe / B200
Pay with a wallet.
No provider key, no account. POST /api/x/rent sells a prepaid block of GPU time for USDC on Base over x402: the first call answers 402 with a challenge whose extra.x402_fee states, in dollars and before any key is touched, the block (rtx4090, 3600 s), the price, the fee (250 bps, $0.01 minimum, $5.00 per lease per UTC day cap), the total, that there are no refunds, and that you are charged only after the lease is live. Sign the EIP-3009 authorization, retry with X-PAYMENT, and the block is placed and settled in that order — never the reverse.
curl -X POST https://compute.pangle.online/api/x/rent \
-H 'Content-Type: application/json' \
-d '{"blocks": 1}' # 402 + the challenge; lease_id is minted here
# then, with the lease_id you were given and the key you will ssh in with:
curl -X POST https://compute.pangle.online/api/x/rent \
-H 'Content-Type: application/json' \
-H "X-PAYMENT: $(base64 -w0 < payload.json)" \
-d '{"blocks": 1, "lease_id": "x402-...", "ssh_pubkey": "ssh-ed25519 AAAA..."}'
What goes in payload.json. The header is base64 of this and nothing else. Every field under authorization except from, validAfter, validBefore and nonce is copied from accepts[0] of the challenge verbatim — a hand-typed to or value is refused. validBefore must clear now + min_validity_seconds (720) or placement is refused before anything is signed for. The signature is EIP-3009 TransferWithAuthorization typed data over the authorization, domain {name: extra.name, version: extra.version, chainId: 8453, verifyingContract: asset}. The binding terms (no refunds, charge only after the lease is live, 30 days' notice on any fee change) are in the frozen profile at /docs/X402-FEE-0.1.md, the URL the challenge itself names as spec_url.
{
"x402Version": 1,
"scheme": "exact",
"network": "base",
"payload": {
"signature": "0x<65-byte EIP-712 signature>",
"authorization": {
"from": "0x<your address>",
"to": "<accepts[0].payTo>",
"value": "<accepts[0].maxAmountRequired>",
"validAfter": "<unix seconds, e.g. now - 60>",
"validBefore": "<unix seconds, at least now + 720>",
"nonce": "0x<32 random bytes, hex>"
}
}
}
x402 v2 sends the same payload under PAYMENT-SIGNATURE with accepted set to the accepts[0] entry you are paying. sidecar/x402-client.py in the repo builds exactly this and is the reference client.
The push feed. The same door sells delivery: POST /api/x/feed answers a 402 for $49 per 30 days (no platform fee, no refunds), and the receipt carries a token — shown once — for GET /api/feed/stream (Authorization: Bearer <token>). Every poll pass, wire move, sealed day and index bin arrives the instant it exists, as SSE events hello, pass, wire, bin, seal, with a : ping every 15 s. Nothing on it is withheld from the free API; you are paying for the push, not the numbers.
curl -X POST https://compute.pangle.online/api/x/feed # 402 + the $49 challenge curl -X POST https://compute.pangle.online/api/x/feed \ -H "X-PAYMENT: $(base64 -w0 < payload.json)" # → receipt with token + stream_url curl -N https://compute.pangle.online/api/feed/stream \ -H "Authorization: Bearer feed_…" # SSE, forever, until expires_at
Try it free first. POST https://402.wick.pics/echo/eip155:8453 is a merchant that never gets paid: a real, signable 402 for 1000 atomic USDC, and on the retry it runs verify only — never settle — and hands back what the facilitator saw, including whether your payer holds the funds on-chain and why a payload was refused. Nothing moves, nothing is charged, no token is needed. An outside client with an empty throwaway wallet reached a correct insufficient_funds refusal from it in one POST; that is the door working.
Delivery and the tape. ssh_pubkey is required on a paying call — a block nobody can enter must not be sold — and the receipt carries the host and port. Your authorization must stay valid for at least 720 seconds; placement takes minutes. Every fill lands on /api/receipts with its transaction hash, from fill number one. Wire contract: x402-fee/0.1, frozen. Facilitator, chains, prices and the payer client: pay.wick.pics/docs.
Ground rules.
Honest data. A provider that stops answering is recorded as down and its rows are flagged stale — never silently reused. Every price passes through one fee seam; fee_bps is currently 0 and the field says so on every response.
Referral disclosure. Outbound provider links may carry referral tags. The renter's price is unchanged; that margin, and the stated fee on blocks settled through us (above), is how a zero-fee index pays for itself.
Courtesy. The money feeds (Vast, RunPod) refresh about every five minutes and the keyless catalogs every thirty, so polling faster than that buys you nothing. For "tell me when", use a watch instead of a loop.