
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.
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 * HOURFirst, 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.
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.
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_BThe 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
}
JSONThe 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.
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"
}
JSONSFC 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.
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.00To 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.
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.00The 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.
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 = $768This 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 daysThe 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 $768For 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.
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.
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.
