SF Compute

Today, on-platform customers on SFC receive an average 25% realized discount off their compute price, via resale.

That means a customer buying $100k of compute receives $25k back from selling idle capacity, on average. For a customer who bought at $4.50/GPU/hr, that brings their realized price to $3.375/GPU/hr.

Purchased / GPU hr$4.500
Realized / GPU hr$4.500
Recovered0%
In useResoldBoughtIdleForecast
Rolling compute reservations and actual demandTime scrolls left past the Now marker. Each row is a node with eight GPUs. Every month the customer buys another month of capacity to maintain six months of runway, adjusting the forecast to recent demand. Buy and sell trades execute now for delivery months in the future: the callouts mark those future dates. Blue blocks are bought and green blocks are sold before delivery. Sold capacity cannot run; delivery uses only the remaining ownership. The dashed blue line shows expected demand, including occasional full months with zero demand. Callouts show each trade's total purchase cost or net sale proceeds. Crossed cells are unfilled actual demand.DeliveredFuture delivery →246810121416Month 1, node 1 (future delivery): reservedMonth 1, node 2 (future delivery): reservedMonth 1, node 3 (future delivery): reservedMonth 1, node 4 (future delivery): reservedMonth 1, node 5 (future delivery): reservedMonth 1, node 6 (future delivery): reservedMonth 1, node 7 (future delivery): reservedMonth 1, node 8 (future delivery): reservedMonth 1, node 9 (future delivery): reservedMonth 1, node 10 (future delivery): reservedMonth 1, node 11 (future delivery): emptyMonth 1, node 12 (future delivery): emptyMonth 1, node 13 (future delivery): emptyMonth 1, node 14 (future delivery): emptyMonth 1, node 15 (future delivery): emptyMonth 1, node 16 (future delivery): emptyMonth 1, node 1 (future delivery): reservedMonth 1, node 2 (future delivery): reservedMonth 1, node 3 (future delivery): reservedMonth 1, node 4 (future delivery): reservedMonth 1, node 5 (future delivery): reservedMonth 1, node 6 (future delivery): reservedMonth 1, node 7 (future delivery): reservedMonth 1, node 8 (future delivery): reservedMonth 1, node 9 (future delivery): reservedMonth 1, node 10 (future delivery): reservedMonth 1, node 11 (future delivery): emptyMonth 1, node 12 (future delivery): emptyMonth 1, node 13 (future delivery): emptyMonth 1, node 14 (future delivery): emptyMonth 1, node 15 (future delivery): emptyMonth 1, node 16 (future delivery): emptyMonth 1, node 1 (future delivery): reservedMonth 1, node 2 (future delivery): reservedMonth 1, node 3 (future delivery): reservedMonth 1, node 4 (future delivery): reservedMonth 1, node 5 (future delivery): reservedMonth 1, node 6 (future delivery): reservedMonth 1, node 7 (future delivery): reservedMonth 1, node 8 (future delivery): reservedMonth 1, node 9 (future delivery): reservedMonth 1, node 10 (future delivery): reservedMonth 1, node 11 (future delivery): emptyMonth 1, node 12 (future delivery): emptyMonth 1, node 13 (future delivery): emptyMonth 1, node 14 (future delivery): emptyMonth 1, node 15 (future delivery): emptyMonth 1, node 16 (future delivery): emptyMonth 1, node 1 (future delivery): reservedMonth 1, node 2 (future delivery): reservedMonth 1, node 3 (future delivery): reservedMonth 1, node 4 (future delivery): reservedMonth 1, node 5 (future delivery): reservedMonth 1, node 6 (future delivery): reservedMonth 1, node 7 (future delivery): reservedMonth 1, node 8 (future delivery): reservedMonth 1, node 9 (future delivery): reservedMonth 1, node 10 (future delivery): reservedMonth 1, node 11 (future delivery): emptyMonth 1, node 12 (future delivery): emptyMonth 1, node 13 (future delivery): emptyMonth 1, node 14 (future delivery): emptyMonth 1, node 15 (future delivery): emptyMonth 1, node 16 (future delivery): emptyMonth 2, node 1 (future delivery): reservedMonth 2, node 2 (future delivery): reservedMonth 2, node 3 (future delivery): reservedMonth 2, node 4 (future delivery): reservedMonth 2, node 5 (future delivery): reservedMonth 2, node 6 (future delivery): reservedMonth 2, node 7 (future delivery): reservedMonth 2, node 8 (future delivery): reservedMonth 2, node 9 (future delivery): reservedMonth 2, node 10 (future delivery): reservedMonth 2, node 11 (future delivery): reservedMonth 2, node 12 (future delivery): reservedMonth 2, node 13 (future delivery): emptyMonth 2, node 14 (future delivery): emptyMonth 2, node 15 (future delivery): emptyMonth 2, node 16 (future delivery): emptyMonth 2, node 1 (future delivery): reservedMonth 2, node 2 (future delivery): reservedMonth 2, node 3 (future delivery): reservedMonth 2, node 4 (future delivery): reservedMonth 2, node 5 (future delivery): reservedMonth 2, node 6 (future delivery): reservedMonth 2, node 7 (future delivery): reservedMonth 2, node 8 (future delivery): reservedMonth 2, node 9 (future delivery): reservedMonth 2, node 10 (future delivery): reservedMonth 2, node 11 (future delivery): reservedMonth 2, node 12 (future delivery): reservedMonth 2, node 13 (future delivery): emptyMonth 2, node 14 (future delivery): emptyMonth 2, node 15 (future delivery): emptyMonth 2, node 16 (future delivery): emptyMonth 2, node 1 (future delivery): reservedMonth 2, node 2 (future delivery): reservedMonth 2, node 3 (future delivery): reservedMonth 2, node 4 (future delivery): reservedMonth 2, node 5 (future delivery): reservedMonth 2, node 6 (future delivery): reservedMonth 2, node 7 (future delivery): reservedMonth 2, node 8 (future delivery): reservedMonth 2, node 9 (future delivery): reservedMonth 2, node 10 (future delivery): reservedMonth 2, node 11 (future delivery): reservedMonth 2, node 12 (future delivery): reservedMonth 2, node 13 (future delivery): emptyMonth 2, node 14 (future delivery): emptyMonth 2, node 15 (future delivery): emptyMonth 2, node 16 (future delivery): emptyMonth 2, node 1 (future delivery): reservedMonth 2, node 2 (future delivery): reservedMonth 2, node 3 (future delivery): reservedMonth 2, node 4 (future delivery): reservedMonth 2, node 5 (future delivery): reservedMonth 2, node 6 (future delivery): reservedMonth 2, node 7 (future delivery): reservedMonth 2, node 8 (future delivery): reservedMonth 2, node 9 (future delivery): reservedMonth 2, node 10 (future delivery): reservedMonth 2, node 11 (future delivery): reservedMonth 2, node 12 (future delivery): reservedMonth 2, node 13 (future delivery): emptyMonth 2, node 14 (future delivery): emptyMonth 2, node 15 (future delivery): emptyMonth 2, node 16 (future delivery): emptyMonth 3, node 1 (future delivery): reservedMonth 3, node 2 (future delivery): reservedMonth 3, node 3 (future delivery): reservedMonth 3, node 4 (future delivery): reservedMonth 3, node 5 (future delivery): reservedMonth 3, node 6 (future delivery): reservedMonth 3, node 7 (future delivery): reservedMonth 3, node 8 (future delivery): reservedMonth 3, node 9 (future delivery): reservedMonth 3, node 10 (future delivery): reservedMonth 3, node 11 (future delivery): reservedMonth 3, node 12 (future delivery): reservedMonth 3, node 13 (future delivery): emptyMonth 3, node 14 (future delivery): emptyMonth 3, node 15 (future delivery): emptyMonth 3, node 16 (future delivery): emptyMonth 3, node 1 (future delivery): reservedMonth 3, node 2 (future delivery): reservedMonth 3, node 3 (future delivery): reservedMonth 3, node 4 (future delivery): reservedMonth 3, node 5 (future delivery): reservedMonth 3, node 6 (future delivery): reservedMonth 3, node 7 (future delivery): reservedMonth 3, node 8 (future delivery): reservedMonth 3, node 9 (future delivery): reservedMonth 3, node 10 (future delivery): reservedMonth 3, node 11 (future delivery): reservedMonth 3, node 12 (future delivery): reservedMonth 3, node 13 (future delivery): emptyMonth 3, node 14 (future delivery): emptyMonth 3, node 15 (future delivery): emptyMonth 3, node 16 (future delivery): emptyMonth 3, node 1 (future delivery): reservedMonth 3, node 2 (future delivery): reservedMonth 3, node 3 (future delivery): reservedMonth 3, node 4 (future delivery): reservedMonth 3, node 5 (future delivery): reservedMonth 3, node 6 (future delivery): reservedMonth 3, node 7 (future delivery): reservedMonth 3, node 8 (future delivery): reservedMonth 3, node 9 (future delivery): reservedMonth 3, node 10 (future delivery): reservedMonth 3, node 11 (future delivery): reservedMonth 3, node 12 (future delivery): reservedMonth 3, node 13 (future delivery): emptyMonth 3, node 14 (future delivery): emptyMonth 3, node 15 (future delivery): emptyMonth 3, node 16 (future delivery): emptyMonth 3, node 1 (future delivery): reservedMonth 3, node 2 (future delivery): reservedMonth 3, node 3 (future delivery): reservedMonth 3, node 4 (future delivery): reservedMonth 3, node 5 (future delivery): reservedMonth 3, node 6 (future delivery): reservedMonth 3, node 7 (future delivery): reservedMonth 3, node 8 (future delivery): reservedMonth 3, node 9 (future delivery): reservedMonth 3, node 10 (future delivery): reservedMonth 3, node 11 (future delivery): reservedMonth 3, node 12 (future delivery): reservedMonth 3, node 13 (future delivery): emptyMonth 3, node 14 (future delivery): emptyMonth 3, node 15 (future delivery): emptyMonth 3, node 16 (future delivery): emptyMonth 4, node 1 (future delivery): reservedMonth 4, node 2 (future delivery): reservedMonth 4, node 3 (future delivery): reservedMonth 4, node 4 (future delivery): reservedMonth 4, node 5 (future delivery): reservedMonth 4, node 6 (future delivery): reservedMonth 4, node 7 (future delivery): reservedMonth 4, node 8 (future delivery): reservedMonth 4, node 9 (future delivery): reservedMonth 4, node 10 (future delivery): reservedMonth 4, node 11 (future delivery): reservedMonth 4, node 12 (future delivery): emptyMonth 4, node 13 (future delivery): emptyMonth 4, node 14 (future delivery): emptyMonth 4, node 15 (future delivery): emptyMonth 4, node 16 (future delivery): emptyMonth 4, node 1 (future delivery): reservedMonth 4, node 2 (future delivery): reservedMonth 4, node 3 (future delivery): reservedMonth 4, node 4 (future delivery): reservedMonth 4, node 5 (future delivery): reservedMonth 4, node 6 (future delivery): reservedMonth 4, node 7 (future delivery): reservedMonth 4, node 8 (future delivery): reservedMonth 4, node 9 (future delivery): reservedMonth 4, node 10 (future delivery): reservedMonth 4, node 11 (future delivery): reservedMonth 4, node 12 (future delivery): emptyMonth 4, node 13 (future delivery): emptyMonth 4, node 14 (future delivery): emptyMonth 4, node 15 (future delivery): emptyMonth 4, node 16 (future delivery): emptyMonth 4, node 1 (future delivery): reservedMonth 4, node 2 (future delivery): reservedMonth 4, node 3 (future delivery): reservedMonth 4, node 4 (future delivery): reservedMonth 4, node 5 (future delivery): reservedMonth 4, node 6 (future delivery): reservedMonth 4, node 7 (future delivery): reservedMonth 4, node 8 (future delivery): reservedMonth 4, node 9 (future delivery): reservedMonth 4, node 10 (future delivery): reservedMonth 4, node 11 (future delivery): reservedMonth 4, node 12 (future delivery): emptyMonth 4, node 13 (future delivery): emptyMonth 4, node 14 (future delivery): emptyMonth 4, node 15 (future delivery): emptyMonth 4, node 16 (future delivery): emptyMonth 4, node 1 (future delivery): reservedMonth 4, node 2 (future delivery): reservedMonth 4, node 3 (future delivery): reservedMonth 4, node 4 (future delivery): reservedMonth 4, node 5 (future delivery): reservedMonth 4, node 6 (future delivery): reservedMonth 4, node 7 (future delivery): reservedMonth 4, node 8 (future delivery): reservedMonth 4, node 9 (future delivery): reservedMonth 4, node 10 (future delivery): reservedMonth 4, node 11 (future delivery): reservedMonth 4, node 12 (future delivery): emptyMonth 4, node 13 (future delivery): emptyMonth 4, node 14 (future delivery): emptyMonth 4, node 15 (future delivery): emptyMonth 4, node 16 (future delivery): emptyMonth 5, node 1 (future delivery): reservedMonth 5, node 2 (future delivery): reservedMonth 5, node 3 (future delivery): reservedMonth 5, node 4 (future delivery): reservedMonth 5, node 5 (future delivery): reservedMonth 5, node 6 (future delivery): reservedMonth 5, node 7 (future delivery): reservedMonth 5, node 8 (future delivery): reservedMonth 5, node 9 (future delivery): reservedMonth 5, node 10 (future delivery): emptyMonth 5, node 11 (future delivery): emptyMonth 5, node 12 (future delivery): emptyMonth 5, node 13 (future delivery): emptyMonth 5, node 14 (future delivery): emptyMonth 5, node 15 (future delivery): emptyMonth 5, node 16 (future delivery): emptyMonth 5, node 1 (future delivery): reservedMonth 5, node 2 (future delivery): reservedMonth 5, node 3 (future delivery): reservedMonth 5, node 4 (future delivery): reservedMonth 5, node 5 (future delivery): reservedMonth 5, node 6 (future delivery): reservedMonth 5, node 7 (future delivery): reservedMonth 5, node 8 (future delivery): reservedMonth 5, node 9 (future delivery): reservedMonth 5, node 10 (future delivery): emptyMonth 5, node 11 (future delivery): emptyMonth 5, node 12 (future delivery): emptyMonth 5, node 13 (future delivery): emptyMonth 5, node 14 (future delivery): emptyMonth 5, node 15 (future delivery): emptyMonth 5, node 16 (future delivery): emptyMonth 5, node 1 (future delivery): reservedMonth 5, node 2 (future delivery): reservedMonth 5, node 3 (future delivery): reservedMonth 5, node 4 (future delivery): reservedMonth 5, node 5 (future delivery): reservedMonth 5, node 6 (future delivery): reservedMonth 5, node 7 (future delivery): reservedMonth 5, node 8 (future delivery): reservedMonth 5, node 9 (future delivery): reservedMonth 5, node 10 (future delivery): emptyMonth 5, node 11 (future delivery): emptyMonth 5, node 12 (future delivery): emptyMonth 5, node 13 (future delivery): emptyMonth 5, node 14 (future delivery): emptyMonth 5, node 15 (future delivery): emptyMonth 5, node 16 (future delivery): emptyMonth 5, node 1 (future delivery): reservedMonth 5, node 2 (future delivery): reservedMonth 5, node 3 (future delivery): reservedMonth 5, node 4 (future delivery): reservedMonth 5, node 5 (future delivery): reservedMonth 5, node 6 (future delivery): reservedMonth 5, node 7 (future delivery): reservedMonth 5, node 8 (future delivery): reservedMonth 5, node 9 (future delivery): reservedMonth 5, node 10 (future delivery): emptyMonth 5, node 11 (future delivery): emptyMonth 5, node 12 (future delivery): emptyMonth 5, node 13 (future delivery): emptyMonth 5, node 14 (future delivery): emptyMonth 5, node 15 (future delivery): emptyMonth 5, node 16 (future delivery): emptyMonth 6, node 1 (future delivery): reservedMonth 6, node 2 (future delivery): reservedMonth 6, node 3 (future delivery): reservedMonth 6, node 4 (future delivery): reservedMonth 6, node 5 (future delivery): reservedMonth 6, node 6 (future delivery): reservedMonth 6, node 7 (future delivery): reservedMonth 6, node 8 (future delivery): reservedMonth 6, node 9 (future delivery): emptyMonth 6, node 10 (future delivery): emptyMonth 6, node 11 (future delivery): emptyMonth 6, node 12 (future delivery): emptyMonth 6, node 13 (future delivery): emptyMonth 6, node 14 (future delivery): emptyMonth 6, node 15 (future delivery): emptyMonth 6, node 16 (future delivery): emptyMonth 6, node 1 (future delivery): reservedMonth 6, node 2 (future delivery): reservedMonth 6, node 3 (future delivery): reservedMonth 6, node 4 (future delivery): reservedMonth 6, node 5 (future delivery): reservedMonth 6, node 6 (future delivery): reservedMonth 6, node 7 (future delivery): reservedMonth 6, node 8 (future delivery): reservedMonth 6, node 9 (future delivery): emptyMonth 6, node 10 (future delivery): emptyMonth 6, node 11 (future delivery): emptyMonth 6, node 12 (future delivery): emptyMonth 6, node 13 (future delivery): emptyMonth 6, node 14 (future delivery): emptyMonth 6, node 15 (future delivery): emptyMonth 6, node 16 (future delivery): emptyMonth 6, node 1 (future delivery): reservedMonth 6, node 2 (future delivery): reservedMonth 6, node 3 (future delivery): reservedMonth 6, node 4 (future delivery): reservedMonth 6, node 5 (future delivery): reservedMonth 6, node 6 (future delivery): reservedMonth 6, node 7 (future delivery): reservedMonth 6, node 8 (future delivery): reservedMonth 6, node 9 (future delivery): emptyMonth 6, node 10 (future delivery): emptyMonth 6, node 11 (future delivery): emptyMonth 6, node 12 (future delivery): emptyMonth 6, node 13 (future delivery): emptyMonth 6, node 14 (future delivery): emptyMonth 6, node 15 (future delivery): emptyMonth 6, node 16 (future delivery): emptyMonth 6, node 1 (future delivery): reservedMonth 6, node 2 (future delivery): reservedMonth 6, node 3 (future delivery): reservedMonth 6, node 4 (future delivery): reservedMonth 6, node 5 (future delivery): reservedMonth 6, node 6 (future delivery): reservedMonth 6, node 7 (future delivery): reservedMonth 6, node 8 (future delivery): reservedMonth 6, node 9 (future delivery): emptyMonth 6, node 10 (future delivery): emptyMonth 6, node 11 (future delivery): emptyMonth 6, node 12 (future delivery): emptyMonth 6, node 13 (future delivery): emptyMonth 6, node 14 (future delivery): emptyMonth 6, node 15 (future delivery): emptyMonth 6, node 16 (future delivery): emptyM1M2M3M4M5M6M7M8M9M10M11Now

To maximize your realized price per GPU hour, SFC implements an order book that lets you get reserved capacity and then sell that capacity later. This lets you safely buy larger quantities, increasing your revenue, without taking the catastrophic risk that other locked in, long term contracts allow.

For most customers, this process is automatic. If you don’t want your node, just turn it off.

However, this article shows how some folks maximally use SFC to maximize their utilization, snatch up very cheap compute, and reduce their overall risk. It walks through how we built givemeanode, our autoresearch product, on top of SFC as a market maker.

Givemeanode turns wholesale GPU capacity into nodes for customer work. Its capacity manager, called the treasury, buys time on SFC and sells time it does not need. It also places bids for future dates.

The examples below build up this process, one decision at a time. Each node has eight GPUs. The treasury limits each order to 24 hours, but delivery can be many days away. The six-month view above shows the broader planning idea.

Prices and budgets below are example values. They are not live quotes or givemeanode's settings. Each purchase or sale is a separate example.

Each section shows the decision in Python.

Use an SFC API token with permission to read the market and manage orders in your pool. A pool holds your compute allocation. A SKU identifies a type of compute.

The examples use the current HTTP API. Market quotes and order estimates use the preview endpoints. The cURL calls show the requests that the Python helper sends.

SFC_API='https://api.sfcompute.com/v2'
SFC_MARKET='https://api.sfcompute.com/preview/v2'
export SFC_TOKEN='YOUR_TOKEN'
 
curl --fail-with-body --silent --show-error \
  "$SFC_API/skus?limit=200" \
  -H "Authorization: Bearer $SFC_TOKEN"

Choose an eligible eight-GPU SKU from the response. Set your pool ID. These times define a 12-hour window that starts tomorrow at 00:00 UTC.

SFC_SKU='sku_REPLACE_ME'
SFC_POOL='pool_REPLACE_ME'
START_AT=$(( ($(date +%s) / 86400 + 1) * 86400 ))
END_AT=$(( START_AT + 12 * 3600 ))

Save market_maker.py beside your Python script. Import it once to use the API helpers. The examples use Decimal for dollar amounts:

import time
from decimal import ROUND_DOWN
from decimal import Decimal as D
from math import ceil
 
import market_maker as market
 
HOUR = 3600
DAY = 24 * HOUR
POOL = "pool_REPLACE_ME"
SKU = "sku_REPLACE_ME"
START_AT = (int(time.time()) // DAY + 1) * DAY
END_AT = START_AT + 12 * HOUR

First, count the GPU slots that need capacity. Include active work, queued work, and a buffer. Subtract supply that comes from outside SFC.

Suppose 24 slots are busy, eight are queued, and eight are needed as a buffer. Eight slots have external supply. The remaining 32 slots need four SFC nodes.

Demand sets a target of four nodesThree busy nodes become a target of five after queued work and a buffer are added. External supply covers one, leaving four SFC nodes and one missing purchase. Each row is one node. Example values. The sequence repeats automatically.NodesDelivery time →1234560h3h6h9h12h3 nodes in use
24 GPUs in use

The full demand calculation also accounts for jobs, reserved capacity, and slots blocked by disks or placement constraints. Those terms are zero in this example.

Add active work, queued work, and the buffer. Then subtract external supply, round up to whole nodes, and subtract nodes already covered:

def nodes_to_buy(required_gpus, external_gpus, owned_nodes, pending_nodes=0):
    sfc_gpus = max(0, required_gpus - external_gpus)
    target_nodes = ceil(sfc_gpus / 8)
    return max(0, target_nodes - owned_nodes - pending_nodes)
 
 
required_gpus = 24 + 8 + 8  # Active work + queue + buffer.
print(nodes_to_buy(required_gpus, external_gpus=8, owned_nodes=3))
# 1 more node: 32 GPU slots need 4 nodes; 3 are already owned.

Each node has eight GPUs, so 33 slots would need five nodes. If one purchase is already pending, set pending_nodes=1. The function then returns zero. Count only healthy owned nodes and pending orders that cover the required time window.

Read the pool to see its allocation over time. Work demand comes from givemeanode's scheduler.

curl --fail-with-body --silent --show-error \
  "$SFC_API/pools/$SFC_POOL" \
  -H "Authorization: Bearer $SFC_TOKEN"

Count pending purchases so the next pass does not buy the same missing node again.

A bid is an offer to buy. An ask is an offer to sell. Both specify a price and a delivery window.

The treasury reads prices for each eligible SKU. It selects the lowest ask within its buy limit. All candidates must meet the work's hardware needs.

Compare prices for the same delivery windowThe selected grid moves from SKU B at sixteen dollars to SKU A at fifteen dollars, then SKU C at fourteen dollars. All selected asks are within the eighteen-dollar buy limit. Each grid shows one node with eight GPU rows. Example values. The sequence repeats automatically.1 node · 8 GPUsSame 12h window2468SKU A · $18 / hrSKU B · $16 / hrSKU C · $21 / hrLowest ask · SKU B
$16 is the lowest ask · limit $18 / node-hour

Here, SKU B costs $16 per node-hour. It is cheaper than SKU A at $18. When prices change, SKU A becomes the next choice.

Choose from eligible SKUs only. If no ask meets the limit, use the preferred SKU for a standing bid at that limit. Dictionary order sets the preference in this example.

def choose_sku(asks, buy_limit):
    if not asks:
        raise ValueError("At least one eligible SKU is required.")
    affordable = [
        sku for sku, ask in asks.items() if ask is not None and ask <= buy_limit
    ]
    if affordable:
        return min(affordable, key=lambda sku: asks[sku])
    # Input order is the configured preference when no ask meets the limit.
    return next(iter(asks))
 
 
asks = {"sku_A": D("18.00"), "sku_B": D("16.00"), "sku_C": D("21.00")}
chosen = choose_sku(asks, buy_limit=D("18.00"))
print(chosen)  # sku_B

The read_asks helper reads one quote per eligible SKU. A missing ask stays None. Use the same delivery window for every SKU:

# Use eligible SKU IDs and the same window for each quote.
# live_asks = market.read_asks([SKU], START_AT, END_AT)
# chosen = choose_sku(live_asks, buy_limit=D("18.00"))

Read the best bid and ask for the exact SKU and delivery window:

curl --fail-with-body --silent --show-error --get \
  "$SFC_MARKET/orderbook/quote" \
  -H "Authorization: Bearer $SFC_TOKEN" \
  --data-urlencode "requirements=instance_sku:$SFC_SKU" \
  --data-urlencode "start_at=$START_AT" \
  --data-urlencode "end_at=$END_AT"

Market depth adds the quantity available at each price level. It helps distinguish a small offer from enough supply for a larger purchase.

curl --fail-with-body --silent --show-error --get \
  "$SFC_MARKET/orderbook/depth" \
  -H "Authorization: Bearer $SFC_TOKEN" \
  --data-urlencode "requirements=instance_sku:$SFC_SKU" \
  --data-urlencode "start_at=$START_AT" \
  --data-urlencode "end_at=$END_AT" \
  --data-urlencode 'depth=5'

An order estimate checks a specific quantity and duration. This request does not place an order. Prices can change before a purchase fills.

curl --fail-with-body --silent --show-error \
  "$SFC_MARKET/order_preview" \
  -H "Authorization: Bearer $SFC_TOKEN" \
  -H 'Content-Type: application/json' \
  --data @- <<JSON
{
  "side": "buy",
  "requirements": {"instance_sku": ["$SFC_SKU"]},
  "pool": "$SFC_POOL",
  "start_at": $START_AT,
  "duration_seconds": 43200,
  "node_count": 1
}
JSON

The treasury buys one node per order. It requires the full requested window. This prevents a partial purchase from leaving gaps in a job's coverage.

If no suitable ask meets the buy limit, the order can wait on the book. That waiting order is a standing bid.

One order buys one complete node windowThe missing node's twelve-hour window changes from unowned to a standing bid, then to a confirmed purchase for two hundred sixteen dollars. Each grid shows one node with eight GPU rows. Example values. The sequence repeats automatically.1 node · 8 GPUsDelivery time →24680h3h6h9h12h1 node missing
Node 4 is missing

This buy order requests one node for 12 hours. Its $18 limit is per node-hour, or $2.25 per GPU-hour for this eight-GPU node. Its maximum cost is $216.

Create an idempotency key once for each new order. Reuse that key and the same request body if you retry the order.

make_order builds one complete node window. It checks the price, hour boundaries, and 24-hour limit. prepare_intent adds a stable key. Save that intent before sending it:

buy = market.make_order(POOL, SKU, "buy", START_AT, END_AT, D("18.00"))
intent = market.prepare_intent(buy)
 
# Save intent in the order journal, then send it:
# order = market.submit_intent(intent)
# A retry must reuse this saved intent.

The helper builds this request body. Here is the equivalent cURL call:

BUY_KEY=$(uuidgen)
 
curl --fail-with-body --silent --show-error \
  "$SFC_API/orders" \
  -H "Authorization: Bearer $SFC_TOKEN" \
  -H "Idempotency-Key: $BUY_KEY" \
  -H 'Content-Type: application/json' \
  --data @- <<JSON
{
  "pool": "$SFC_POOL",
  "side": "buy",
  "allow_standing": true,
  "allow_partial": false,
  "sku": "$SFC_SKU",
  "allocation_schedule_delta": [{
    "start_at": $START_AT,
    "end_at": $END_AT,
    "node_count": 1
  }],
  "limit_price_dollars_per_node_hour": "18.00"
}
JSON

SFC holds credit for the order at its limit price. A confirmed fill adds capacity for the specified hours. An open bid alone does not give the customer capacity.

Use the order ID from the response to read its state and fills:

ORDER_ID='ordr_REPLACE_ME'
 
curl --fail-with-body --silent --show-error \
  "$SFC_API/orders/$ORDER_ID" \
  -H "Authorization: Bearer $SFC_TOKEN"

An occupied node may need to run beyond its current end time. The treasury buys an extension on the same SKU and in the same pool. The new window starts where the existing window ends.

An extension adds time to an occupied nodeA six-hour bid appears beside twelve hours of owned time. The bid fills and joins the original window, extending coverage to eighteen hours. Each grid shows one node with eight GPU rows. Example values. The sequence repeats automatically.1 node · 8 GPUsDelivery time →24680h6h12h18h6 more hours needed
Work needs 6 more hours

This order adds six hours to one node. It does not add a second node during the original window.

def extension_window(now, coverage_end, target_hours, occupied):
    if not occupied or coverage_end <= now:
        return None
    shortfall = now + target_hours * HOUR - coverage_end
    hours_to_add = min(24, max(0, (shortfall + HOUR - 1) // HOUR))
    if hours_to_add == 0:
        return None
    return coverage_end, coverage_end + hours_to_add * HOUR
 
 
# Review this example at the start of the current window.
window = extension_window(START_AT, END_AT, target_hours=18, occupied=True)
if window:
    extension = market.make_order(POOL, SKU, "buy", *window, D("18.00"))
    # Keep the existing node's pool and SKU.
    extension_intent = market.prepare_intent(extension)

As the deadline approaches, the extension policy can raise the buy limit. It stays within a configured maximum. Empty nodes do not receive automatic extensions merely because their time is ending.

This price function starts at a floor. It reaches the cap before coverage ends, leaving time for the fill and node setup. The example uses a 45-minute lead and a two-hour price ramp.

def deadline_limit(minutes_left, floor, cap, lead_minutes=45, ramp_minutes=120):
    cap = max(D(0), cap)
    floor = max(D(0), min(floor, cap))
    over = minutes_left - lead_minutes
    if over <= 0 or ramp_minutes <= 0:
        return cap
    if over >= ramp_minutes:
        return floor
    price = floor + (cap - floor) * D(ramp_minutes - over) / D(ramp_minutes)
    return price.quantize(D("0.01"), rounding=ROUND_DOWN)
 
 
for minutes_left in [180, 105, 45]:
    print(deadline_limit(minutes_left, floor=D("12.00"), cap=D("18.00")))
# 12.00, 15.00, 18.00

To change a standing order, cancel it and confirm its final fills before you replace it. Section 8 shows that sequence.

Suppose a node is empty and its last four hours are no longer needed. The treasury can offer those hours for sale now, before delivery starts.

It first checks the work and reserve requirements. It blocks new work on the node before it submits the sale. A low GPU utilization reading alone is not enough: disks and queued work can still require the node.

Sell spare time while keeping customer work protectedThe last four hours of a twelve-hour window change from idle capacity to an open offer. A fill transfers those hours to a buyer for thirty-two dollars before fees. Each grid shows one node with eight GPU rows. Example values. The sequence repeats automatically.1 node · 8 GPUsDelivery time →24680h4h8h12h4 spare hours
Keep 8 hours · the last 4 hours are idle

The resale policy leaves time before delivery to observe a fill and end the seller's use. This example offers the last four hours of the original 12-hour window. The pool must own those hours and have them available for sale.

First, check that the sale keeps work and reserve capacity protected. Then select the last four hours, with at least one hour before delivery.

def can_sell_idle(node, fleet, now):
    protected = (
        node["busy_gpus"] > 0
        or node["parked_disks"] > 0
        or node["wake_pending"]
        or node["listed"]
        or node["retiring"]
        or node["job_protected_until"] > now
    )
    return (
        not protected
        and fleet["empty_nodes"] - 1 >= fleet["minimum_empty"]
        and fleet["nodes"] - 1 >= fleet["minimum_nodes"]
    )
 
 
def tail_window(now, owned_start, owned_end, hours_to_sell=4):
    earliest = ((now + HOUR + HOUR - 1) // HOUR) * HOUR
    start = max(owned_start, owned_end - hours_to_sell * HOUR, earliest)
    if start >= owned_end or owned_end - start > DAY:
        return None
    return start, owned_end
 
 
node = {
    "busy_gpus": 0, "parked_disks": 0, "wake_pending": False,
    "listed": False, "retiring": False, "job_protected_until": 0,
}
fleet = {"nodes": 4, "empty_nodes": 2, "minimum_nodes": 2, "minimum_empty": 1}
 
if can_sell_idle(node, fleet, now=START_AT):
    window = tail_window(START_AT, START_AT, END_AT, hours_to_sell=4)
    if window:
        sale = market.make_order(POOL, SKU, "sell", *window, D("8.00"))
        sale_intent = market.prepare_intent(sale)

In the running service, the scheduler locks the node, checks occupancy again, blocks new placements, and records the sale intent in one transaction. It sends the order after that transaction commits.

At a fill price of $8 per node-hour, the four-hour sale returns $32 before fees. The sell limit is a minimum price. Resale fees reduce the credit received. A listing earns nothing until it fills.

The treasury can lower an ask as unused time approaches. Its price schedule keeps track of earlier price reductions, so a replacement listing does not restart at the original high price.

For a short resale window, a price ramp can also stay just below the best ask. Keep the same elapsed time when the listing is replaced.

def resale_price(elapsed_minutes, start, floor, best_ask=None, ramp_minutes=45):
    if floor <= 0 or start < floor or ramp_minutes <= 0:
        raise ValueError("Use positive prices and a positive price-ramp duration.")
    elapsed = D(min(ramp_minutes, max(0, elapsed_minutes)))
    price = start - (start - floor) * elapsed / D(ramp_minutes)
    if best_ask is not None:
        price = min(price, max(floor, best_ask * D("0.99")))
    return max(floor, price.quantize(D("0.01"), rounding=ROUND_DOWN))
 
 
print(resale_price(0, start=D("12.00"), floor=D("4.00")))
print(resale_price(45, start=D("12.00"), floor=D("4.00")))
# 12.00, 4.00

The treasury also buys ahead of confirmed demand. It divides future time into 24-hour UTC blocks and sets an hourly budget for each block.

Near dates receive a larger budget than distant dates. A continuous day can receive a higher bid than a short remaining window. Lower bids use the rest of the budget to seek more nodes.

Place budget-limited bids for future daysOne sixteen-dollar bid for a future day is joined by two eight-dollar bids. The higher bid fills while the other two remain open. Total committed cost stays within seven hundred sixty-eight dollars. Each row is one node. Example values. The sequence repeats automatically.NodesDelivery time →12340h6h12h18h24h1 future bid
$16 / hour × 24 hours = $384 held

In this example, a $32 hourly budget supports one $16 bid and two $8 bids. All three could fill. The maximum credit hold for the day is $768.

The higher bid values a continuous window. The remaining budget buys more nodes at the lower price. Subtract commitments already in the same time block before adding bids.

def future_ladder(hourly_budget, premium, floor, committed=D(0), premium_exists=False):
    remaining = max(D(0), hourly_budget - committed)
    if floor <= 0 or premium < floor:
        return []
    if premium == floor:
        return [(floor, int(remaining // floor))] if remaining >= floor else []
    premium_nodes = int((remaining / 2) // premium)
    if premium_nodes == 0 and remaining >= premium and not premium_exists:
        premium_nodes = 1
    floor_nodes = int((remaining - premium_nodes * premium) // floor)
    return [
        (price, count)
        for price, count in [(premium, premium_nodes), (floor, floor_nodes)]
        if count > 0
    ]
 
 
days_out = 6
hourly_budget = D("32.00") if days_out <= 7 else D("8.00")
levels = future_ladder(hourly_budget, premium=D("16.00"), floor=D("8.00"))
print(levels)
# [(Decimal("16.00"), 1), (Decimal("8.00"), 2)]
1 node × $16/hour × 24 hours = $384
2 nodes × $8/hour × 24 hours = $384
Total possible cost          = $768

This standing bid requests a full day, about one week from now. It can fill today even though its compute arrives later.

The $16 and $8 limits in the diagram are already set for its example date. The pricing function below shows how a limit can change with the date and the length of a continuous window.

def future_prices(days_out, window_hours, floor, cap):
    days = D(min(60, max(0, days_out)))
    factor = D(1) - D("0.75") * days / D(60)
    future_floor, future_cap = floor * factor, cap * factor
    hours = D(min(24, max(2, window_hours)))
    premium = future_floor + (future_cap - future_floor) * (hours - 2) / 22
    return (
        future_floor.quantize(D("0.01"), rounding=ROUND_DOWN),
        premium.quantize(D("0.01"), rounding=ROUND_DOWN),
    )
 
 
print(future_prices(0, 24, floor=D("8.00"), cap=D("16.00")))
print(future_prices(60, 24, floor=D("8.00"), cap=D("16.00")))
# (8.00, 16.00) today; (2.00, 4.00) at 60 days

The future-bid policy counts open orders at their limit prices. It also keeps credit for required purchases and applies a daily spending limit. A cheap bid still needs cash if it fills.

Reserve enough available credit for two required 24-hour purchases per GPU model. Available balance here already excludes existing holds. The daily limit separately counts money spent and all open purchase commitments.

def future_cash(
    available_balance, spent_today, open_commitment, daily_cap, max_buy_by_model
):
    required_reserve = sum(2 * 24 * price for price in max_buy_by_model)
    cash_room = available_balance - required_reserve
    cap_room = daily_cap - spent_today - open_commitment
    return max(D(0), min(cash_room, cap_room))
 
 
def fit_bids(levels, hours, cash):
    orders = []
    for price, count in levels:
        for _ in range(count):
            cost = price * hours
            if cost > cash:
                break
            orders.append(price)
            cash -= cost
    return orders
 
 
cash = future_cash(
    available_balance=D("2000.00"), spent_today=D("200.00"),
    open_commitment=D("400.00"), daily_cap=D("1500.00"),
    max_buy_by_model=[D("18.00")],
)
prices = fit_bids(levels, hours=24, cash=cash)
future_start = START_AT + days_out * DAY
future_orders = [
    market.prepare_intent(market.make_order(
        POOL, SKU, "buy", future_start, future_start + DAY, price
    ))
    for price in prices
]
print(len(future_orders))  # 3 orders; maximum total cost $768

For multiple dates, plan the higher continuous-window bids first, nearest date first. Then use the remaining cash for lower bids. The near-date and far-date hourly budgets are separate inputs.

A reservation does not have to be kept or sold as one unit. The treasury can keep the start, sell a middle window, and keep time after the gap.

In this example, the customer keeps two hours, sells six, and keeps the final four hours.

Sell the middle of a window and keep both endsA twelve-hour window splits into two kept hours, six hours offered for sale, and four kept hours. When the sale fills, the middle turns green and leaves a gap in the seller's coverage. Each grid shows one node with eight GPU rows. Example values. The sequence repeats automatically.1 node · 8 GPUsDelivery time →24680h2h8h12h6 hours in the middle
One node · a continuous 12-hour window

This needs coordination with the scheduler. Work must stop before the sold window starts. Jobs that support saved progress can resume on available capacity. The seller cannot use the node during sold hours.

The middle-window policy keeps two hours at the start and at least three hours at the end. It offers at most six hours in the middle. It also checks committed demand, replacement cost, and fees before a sale.

The order below shows the market request with future delivery times. In givemeanode, this policy evaluates the remaining time on a node that is already running. The application must enforce the work deadline and manage jobs across the gap. This is a separate sale example; do not combine it with an overlapping listing.

def slice_plan(remaining_hours, committed_percent, active_gaps, can_stop_work):
    if committed_percent >= 50 or active_gaps >= 2 or not can_stop_work:
        return None
    sell_hours = min(6, remaining_hours // 2, remaining_hours - 2 - 3)
    if sell_hours < 1:
        return None
    return {
        "keep_start": 2,
        "sell": sell_hours,
        "keep_end": remaining_hours - 2 - sell_hours,
    }
 
 
gap = slice_plan(
    remaining_hours=12, committed_percent=20,
    active_gaps=0, can_stop_work=True,
)
print(gap)  # {"keep_start": 2, "sell": 6, "keep_end": 4}
 
if gap:
    gap_start = START_AT + gap["keep_start"] * HOUR
    gap_end = gap_start + gap["sell"] * HOUR
    middle_sale = market.make_order(
        POOL, SKU, "sell", gap_start, gap_end, D("8.00"), partial=True
    )

The time plan is one check. The sale price must also cover replacement work, the move, and fees. This simplified price floor uses a 10% margin. The running policy also checks observed market liquidity and replacement purchases.

def slice_price_floor(
    replacement_cost, switching_cost, hours, fee_rate, margin=D("0.10")
):
    if hours <= 0 or not 0 <= fee_rate < 1:
        raise ValueError("Use positive hours and a fee rate below one.")
    net_required = (replacement_cost + switching_cost) * (1 + margin)
    # Round up so the gross price still covers the required net amount.
    from decimal import ROUND_UP
 
    return (net_required / (hours * (1 - fee_rate))).quantize(
        D("0.01"), rounding=ROUND_UP
    )
 
 
minimum_price = slice_price_floor(
    replacement_cost=D("24.00"), switching_cost=D("8.00"),
    hours=6, fee_rate=D("0.10"),
)
print(minimum_price)  # 6.52 per node-hour
# The example's $8 sell limit is above this estimated floor.

For nodes with work to move, this policy permits partial fills when the price covers that move. Empty nodes use complete-window sales. After the first partial fill, the treasury requests cancellation of the rest. Further fills can arrive before cancellation completes. The scheduler accounts for all fills and keeps work outside the resulting gap.

Orders and cancellations take time. A cancellation request can reach the exchange while a buyer is filling the order.

Suppose two hours sell during cancellation. Those two hours stay sold. Only the unfilled four hours become available again.

A cancellation request can race with a fillCancellation is requested for six listed hours. Two hours fill before cancellation completes. The two filled hours stay sold, while the four unfilled hours return to the seller. Each grid shows one node with eight GPU rows. Example values. The sequence repeats automatically.1 node · 8 GPUsDelivery time →24680h2h4h6hCancel requested
6 hours are still listed while cancellation is pending

Set ORDER_ID to the sell order's ID. Request cancellation, then read the order again:

ORDER_ID='ordr_REPLACE_ME'
 
curl --fail-with-body --silent --show-error \
  -X POST "$SFC_API/orders/$ORDER_ID/cancel" \
  -H "Authorization: Bearer $SFC_TOKEN"
 
curl --fail-with-body --silent --show-error \
  "$SFC_API/orders/$ORDER_ID" \
  -H "Authorization: Bearer $SFC_TOKEN"

A successful cancellation response means the request was accepted. Continue reading until the order has a final state, such as cancelled or filled. Check fills and filled_allocation_schedule_delta before you reuse capacity or replace the order. The order documentation describes this lifecycle.

The cancel_and_read helper reads before cancelling, handles a fill during cancellation, and waits for a final state. If it times out, keep the intent pending. Do not release its capacity or create a replacement yet.

# Read the order, request cancellation, and wait for its final state.
# final_order = market.cancel_and_read("ordr_REPLACE_ME")

For this one-node sale, subtract the final filled intervals from the requested window. The unfilled_windows helper also handles fills in separate parts of the window. This sample final response shows two hours sold:

snapshot = {
    "state": "cancelled",
    "filled_allocation_schedule_delta": [
        {"start_at": 0, "end_at": 2 * HOUR, "node_count": 1},
        {"start_at": 2 * HOUR, "end_at": None, "node_count": 0},
    ],
}
free = market.unfilled_windows(0, 6 * HOUR, snapshot)
print([(start // HOUR, end // HOUR) for start, end in free])
# [(2, 6)]: only the last four hours return to the seller.

The treasury saves an order's intent before it sends the request. Its order reconciliation compares local records with the exchange. If a response is lost, it checks the order before it submits another purchase.

Each pass starts with the latest work, allocations, and fills. It then updates the plan within its capacity and cash limits:

Read work, allocations, orders, and fills.
Resolve pending orders and cancellations.
Measure the capacity that work needs.
Buy missing nodes and extend occupied nodes.
Offer spare time for sale.
Place future bids within the remaining budget.
Repeat as work and prices change.

The result is a capacity plan that can change after purchase. Customers can secure time ahead of demand, add time when work grows, and recover money when plans change.

Sign up for SFC →

Subscribe to updates

Point Mugu, CA