# Silicon Liquid API

> JSON over HTTPS at https://siliconliquid.com/api/v1. GPU prices, listings and availability need no key; a party's own orders, requests, deals, allocations, events and webhooks need an API key sent as `Authorization: Bearer sl_live_...`. Silicon Liquid takes no payments: suppliers invoice and collect.

## Authentication

A key acts for one party and sees only that party's objects (anything else is a 404). It does nothing until the desk attaches a scope, and expires after 90 days by default. A write scope includes the read scope of the same thing.

- `orders:read`: Read the party's GPU orders and estimate new ones
- `orders:write`: Place GPU orders (Buy now, holding the GPUs)
- `rfqs:read`: Read the party's requests, with their replies
- `rfqs:write`: Publish requests and accept quotes
- `deals:read`: Read the party's long-term contract requests
- `deals:write`: Send long-term contract requests to the desk
- `delivery:read`: Read the party's allocations and delivery certificates
- `events:read`: Read the party's events feed
- `webhooks:read`: List the party's webhook endpoints
- `webhooks:write`: Add and remove webhook endpoints

## Idempotency

Every POST takes an `Idempotency-Key` header (a fresh UUID per request); writes require one. The same key and request within 24 hours returns the first answer; the same key with a different request is a 422; the same key while the first runs is a 409.

## Errors

`{ "error": { "type", "message", "issues"?, "requestId"? } }`. Branch on type.

- `invalid_request`: HTTP 400
- `authentication_required`: HTTP 401
- `invalid_api_key`: HTTP 401
- `insufficient_scope`: HTTP 403
- `party_blocked`: HTTP 403
- `not_found`: HTTP 404
- `method_not_allowed`: HTTP 405
- `conflict`: HTTP 409
- `precondition_failed`: HTTP 412
- `idempotency_conflict`: HTTP 422
- `rate_limited`: HTTP 429
- `internal_error`: HTTP 500
- `unavailable`: HTTP 503

## Pagination

Lists take `limit` and `starting_after` and answer `{ data, hasMore, nextCursor }`; pass nextCursor as starting_after for the next page. GET /events is oldest first and append-only: poll with the last nextCursor to see only what is new.

## Webhooks

POST /webhook-endpoints with an https URL (and optional eventTypes, exact or a prefix ending in "."). Every event the party could read at GET /events is then POSTed to it. The body is the event:

```json
{
  "id": "evt_1204",
  "seq": 1204,
  "type": "order.created_via_api",
  "createdAt": "2026-10-19T09:30:12.418Z",
  "actor": {
    "type": "api_key",
    "id": "sl_live_ab12cd34"
  },
  "object": {
    "kind": "order",
    "id": "k7m2q9xd"
  },
  "data": {
    "keyId": "0b8f6a52-7c3e-4c1d-9a7e-5d2f1e3c4b6a",
    "offer": "l40",
    "gpus": 8,
    "listingId": null
  },
  "hash": "9f2c4e1b7a…",
  "prevHash": "41d0e8c3b2…"
}
```

- Every delivery carries `SL-Signature: t=<unix seconds>,v1=<hex>`. v1 is the HMAC-SHA256, keyed with the endpoint's whole secret (whsec_ included), of the timestamp, a full stop and the raw request body exactly as received: `t + "." + body`.
- Compute it yourself and compare in constant time; accept the request when any v1 in the header matches (during a secret rotation with an overlap there are two).
- Refuse a t more than 5 minutes from your clock, so a captured request cannot be replayed later.
- `SL-Event-Id` is the event's id and is the same on every attempt: use it to drop repeats. `SL-Event-Type` is its type.
- Answer 2xx within 10 seconds. Anything else, a redirect included, is retried: up to 8 attempts over about 24 hours. An endpoint that fails every attempt for three days is disabled.

```js
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.SL_WEBHOOK_SECRET; // whsec_...
const app = express();

// Read the raw body: the signature covers the exact bytes sent.
app.post("/silicon-liquid", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString("utf8");
  if (!verify(req.get("SL-Signature") ?? "", body, SECRET)) return res.sendStatus(400);
  const event = JSON.parse(body);
  // SL-Event-Id (= event.id) is the same on every retry: skip ones you have seen.
  console.log(event.type, event.object);
  res.sendStatus(200); // answer within 10 s; do slow work afterwards
});

function verify(header, body, secret, toleranceSeconds = 300) {
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = Number(parts.find(([k]) => k === "t")?.[1]);
  const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || !sigs.length) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const want = crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest();
  return sigs.some((s) => {
    const got = Buffer.from(s, "hex");
    return got.length === want.length && crypto.timingSafeEqual(got, want);
  });
}

app.listen(3000);
```

```python
import hashlib, hmac, json, os, time
from flask import Flask, request, abort

SECRET = os.environ["SL_WEBHOOK_SECRET"]  # whsec_...
app = Flask(__name__)

def verify(header: str, body: bytes, secret: str, tolerance: int = 300) -> bool:
    parts = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
    t = next((v for k, v in parts if k == "t"), None)
    sigs = [v for k, v in parts if k == "v1"]
    if not t or not t.isdigit() or not sigs or abs(time.time() - int(t)) > tolerance:
        return False
    want = hmac.new(secret.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(want, s) for s in sigs)

@app.post("/silicon-liquid")
def hook():
    body = request.get_data()  # the raw bytes, before any JSON parsing
    if not verify(request.headers.get("SL-Signature", ""), body, SECRET):
        abort(400)
    event = json.loads(body)
    # SL-Event-Type says the type; SL-Event-Id repeats on retries.
    print(event["type"], event["object"])
    return "", 200
```

## MCP

A read-only MCP server (Streamable HTTP, stateless JSON-RPC) at https://siliconliquid.com/api/mcp. Auth: the same API key as a bearer token; public tools work without one.

```sh
claude mcp add --transport http silicon-liquid https://siliconliquid.com/api/mcp \
  --header "Authorization: Bearer $SL_API_KEY"
```

- `get_prices`: The public GPU price reference: per chip, today's on-demand median across providers, spot and reserved prices, and the day and week changes. With a chip, each provider's line and the daily history. Sources whose terms allow republishing only.
- `get_availability`: For every listing on sale, how many GPUs (pools) or servers (windows) are free in each hour of the next eight weeks, with the price per GPU-hour. Filter by chip, kind or the least that must be free.
- `recommend`: Turns a plain-language need into one recommended package and at most two others: what you get in plain words, how to buy it, an all-in monthly estimate with its source and a real start date. Map the user's words onto the fields: job (train, fine-tune, serve...), size (by model size in billions of parameters, by GPU count, by monthly budget, or unsure), start (now, a date, or unsure) and term.
- `list_listings`: Open listings with their chip, price, capacity, delivery class, evidence level and next eight weeks. Never the supplier or its floor price.
- `get_listing`: One listing by its uuid, or by a catalog offer id ("l40", "l40g"). An ended listing still answers, with onSale false.
- `estimate_order` (needs orders:read): What an order of some GPUs on one listing costs per hour, day and month, and for a window of whole hours (UTC), and whether the GPUs are free now. Holds nothing and places nothing.
- `my_orders` (needs orders:read): The orders this key's party placed through the API: their status in plain words, price and GPUs. With a ref, one order and its history.
- `my_allocations` (needs delivery:read): The capacity this key's party has been allocated (as buyer) or is delivering (as supplier): state, window, nodes, price and delivery certificates. With an id, one allocation with its history and access grants.
- `get_certificate` (needs delivery:read): A signed acceptance-test result for one of the party's allocations: every stage and its metrics, the hash chain and the Ed25519 signature.

## Endpoints

### Catalog

#### GET /skus

List the SKUs the venue sells. Chip, GPUs per server, memory and fabric: two servers of one SKU are interchangeable.

Scope: none (anonymous).

```sh
curl "https://siliconliquid.com/api/v1/skus"
```

#### GET /availability

What is free on each listing, week by week. For every listing on sale, what is free in every hour of each of the next eight weeks (GPUs for a pool, whole servers for a window). Week 0 starts today at 00:00 UTC.

Scope: none (anonymous).

```sh
curl "https://siliconliquid.com/api/v1/availability?chip=l40"
```

#### GET /listings

List what is on sale. Open listings with their price, capacity, delivery class, evidence level and next eight weeks. Never the supplier or its floor.

Scope: none (anonymous).

```sh
curl "https://siliconliquid.com/api/v1/listings?kind=pool"
```

#### GET /listings/{id}

One listing. By its uuid, or by a catalog offer id (l40, l40g). Ended listings still answer, with onSale false.

Scope: none (anonymous).

```sh
curl "https://siliconliquid.com/api/v1/listings/l40"
```

#### POST /recommend

Three questions in, one package out. What you want to do, how big, and when and for how long. Answers with one recommended package and at most two others: what you get in plain words, the lane that buys it, an all-in monthly estimate with its source and a real start date.

Scope: none (anonymous).

```sh
curl -X POST "https://siliconliquid.com/api/v1/recommend" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"job":"train","size":{"by":"model","paramsB":8},"start":{"by":"now"},"term":"weeks"}'
```

### Prices

#### GET /prices

The public price reference, every chip. Today's market per chip from the sources whose terms allow republishing: the on-demand median across providers, spot, reserved (never blended) and the day and week changes.

Scope: none (anonymous).

```sh
curl "https://siliconliquid.com/api/v1/prices"
```

#### GET /prices/{chip}

The public price reference for one chip. Each provider's line and the daily history, public sources only.

Scope: none (anonymous).

```sh
curl "https://siliconliquid.com/api/v1/prices/h100"
```

### Orders

#### POST /orders/estimate

Price an order before placing it. What an order on a listing costs per hour, day and month, and for a window of whole hours, and whether the GPUs are free now. Nothing is held.

Scope: orders:read.

```sh
curl -X POST "https://siliconliquid.com/api/v1/orders/estimate" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"listing":"l40","gpus":8,"startAt":"2026-11-02T14:00:00Z","endAt":"2026-11-09T14:00:00Z"}'
```

#### POST /orders

Place an order (Buy now). Buy now on a listed pool: the GPUs are held for 12 hours in the same transaction as the order, or the order is refused with what is free. Nothing is charged: once the desk confirms, the supplier invoices the buyer directly. Requires an Idempotency-Key.

Scope: orders:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/orders" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"listing":"6f1c2a9e-4b7d-4e2a-9c1f-2d3e4f5a6b7c","offer":"l40","gpus":8,"preset":"qwen3-30b","email":"ops@acme.ai","company":"Acme AI","terms":true}'
```

#### GET /orders

The party's orders. Orders placed with this party's keys, newest first.

Scope: orders:read.

```sh
curl "https://siliconliquid.com/api/v1/orders" \
  -H "Authorization: Bearer $SL_API_KEY"
```

#### GET /orders/{ref}

One order. Its status in the buyer's words, its hold and its history. Another party's order is a 404.

Scope: orders:read.

```sh
curl "https://siliconliquid.com/api/v1/orders/abcd2345" \
  -H "Authorization: Bearer $SL_API_KEY"
```

### Requests

#### POST /rfqs

Publish a request for quotes. The same request the /rfq form publishes: suppliers quote on its public link, and the replies are this party's alone. The owner key in the answer opens the replies page in a browser; the API needs only the API key. Requires an Idempotency-Key.

Scope: rfqs:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/rfqs" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"spec":{"kind":"compute","side":"BUY","gpu":"H100","gpusPerNode":8,"units":8,"gpus":64,"country":"US","termMonths":3,"priceMode":"R"}}'
```

#### GET /rfqs/{ref}

One request, with its replies for the owner. Anyone with the scope reads the public request; the party that published it also gets the replies, ranked by the store, with suppliers named only once awarded.

Scope: rfqs:read.

```sh
curl "https://siliconliquid.com/api/v1/rfqs/abcd2345" \
  -H "Authorization: Bearer $SL_API_KEY"
```

#### POST /rfqs/{ref}/awards

Accept a quote. Accept one firm quote; call again for more until the request is covered. Refused unless the quote can still be taken and both compliance packs exist. Requires an Idempotency-Key.

Scope: rfqs:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/rfqs/abcd2345/awards" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"quoteId":42}'
```

### Deals

#### POST /deals

Ask the desk for a long-term contract. A Data-centre direct request (6 to 60 months from partner data centres). A named person replies the same business day; the room shows anonymous options within two business days. Requires an Idempotency-Key.

Scope: deals:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/deals" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"job":"train","gpu":"B200","nodes":32,"termMonths":24,"region":"north_america","email":"infra@acme.ai","company":"Acme AI"}'
```

#### GET /deals

The party's long-term contract requests.

Scope: deals:read.

```sh
curl "https://siliconliquid.com/api/v1/deals" \
  -H "Authorization: Bearer $SL_API_KEY"
```

#### GET /deals/{ref}

One deal room, as the buyer sees it. Its step, timeline, the anonymous options and the messages and documents for the buyer. Another party's deal is a 404.

Scope: deals:read.

```sh
curl "https://siliconliquid.com/api/v1/deals/abcd2345" \
  -H "Authorization: Bearer $SL_API_KEY"
```

#### POST /deals/{ref}/interest

Say you are interested in one anonymous option. Starts the NDA step for that option: your NDA and the data centre's, each to sign by email code; then the desk passes names. Saying it twice changes nothing. There is no payment endpoint: the data centre invoices you directly. Requires an Idempotency-Key.

Scope: deals:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/deals/abcd2345/interest" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"optionId":"6f1c2a9e-4b7d-4e2a-9c1f-2d3e4f5a6b7c"}'
```

### Delivery

#### GET /allocations/{id}

One allocation. Its state, history, certificates and access grants, for the buyer or the supplier behind it.

Scope: delivery:read.

```sh
curl "https://siliconliquid.com/api/v1/allocations/17" \
  -H "Authorization: Bearer $SL_API_KEY"
```

#### GET /certificates/{id}

One delivery certificate. The signed acceptance-test result: every stage and its metrics, the hash chain and the Ed25519 signature.

Scope: delivery:read.

```sh
curl "https://siliconliquid.com/api/v1/certificates/5" \
  -H "Authorization: Bearer $SL_API_KEY"
```

### Events

#### GET /events

The party's events, oldest first. Append-only, read forward with starting_after: every audit-chain row about this party or its orders, requests and deals, with who acted. Poll with the last nextCursor to see only what is new.

Scope: events:read.

```sh
curl "https://siliconliquid.com/api/v1/events?starting_after=evt_120&limit=50" \
  -H "Authorization: Bearer $SL_API_KEY"
```

### Webhooks

#### POST /webhook-endpoints

Add a webhook endpoint. Every event this party could read at GET /events is also POSTed here, signed with the secret in the answer (shown once). https only, on a public address. At most 16 per party. Requires an Idempotency-Key.

Scope: webhooks:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.acme.ai/silicon-liquid","description":"Ops alerts","eventTypes":["order.","rfq."]}'
```

#### GET /webhook-endpoints

The party's webhook endpoints. Each with its health: the last success and failure, failures in a row, and deliveries still to go. Never the secret.

Scope: webhooks:read.

```sh
curl "https://siliconliquid.com/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $SL_API_KEY"
```

#### POST /webhook-endpoints/{id}

Change, disable or re-enable an endpoint, or rotate its secret. Fields left out stay as they are. rotateSecret answers with the new secret, once; expirePreviousInHours keeps the old one signing alongside it for up to 72 hours so receivers can switch without dropping events. Requires an Idempotency-Key.

Scope: webhooks:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/webhook-endpoints/0b8f6a52-7c3e-4c1d-9a7e-5d2f1e3c4b6a" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"rotateSecret":true,"expirePreviousInHours":24}'
```

#### DELETE /webhook-endpoints/{id}

Delete an endpoint. It stops at once; deliveries still waiting to be sent to it are dropped. Its events stay in GET /events.

Scope: webhooks:write.

```sh
curl -X DELETE "https://siliconliquid.com/api/v1/webhook-endpoints/0b8f6a52-7c3e-4c1d-9a7e-5d2f1e3c4b6a" \
  -H "Authorization: Bearer $SL_API_KEY"
```

#### POST /webhook-endpoints/{id}/test

Send a test event now. A signed "webhook_endpoint.test" event, sent at once and answered with what the endpoint said. Never retried, and never counted toward the endpoint's failures.

Scope: webhooks:write.

```sh
curl -X POST "https://siliconliquid.com/api/v1/webhook-endpoints/0b8f6a52-7c3e-4c1d-9a7e-5d2f1e3c4b6a/test" \
  -H "Authorization: Bearer $SL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```
