Provider Keys — your first call
Three providers are live: Fal, Wavespeed and ElevenLabs. You call them with the Surplus API key you already have, and the discount comes off the provider's own list price. There is no second credential to collect and nothing to reserve.
This page is the end-to-end path for all three. The per-provider pages (Fal, ElevenLabs, Wavespeed) then give each one's exact routes and bodies.
- Your requestSurplus key, provider's own body
- FenceHost and route checked first
- OfferCheapest live key that may serve
- Price & holdQuoted, then held on your credit
- Provider answersCharged actual, headers say what
1. Get a key and credit
Any Surplus API key works — the same one you use for inference. The key market is prepaid credit only: it never falls through to the on-chain rail, so a wallet with USDC but no credit is refused rather than quietly charged on chain.
export SURPLUS_API_KEY=sk_live_…
curl $SURPLUS_BASE_URL/payments/balance \
-H "Authorization: Bearer $SURPLUS_API_KEY"If the balance cannot cover the quote, the call answers 402 insufficient_credit and nothing is reserved.
2. Find out what is live
The book is public — no key needed — so you can check before you spend.
curl $SURPLUS_BASE_URL/resource/markets
{
"markets": [
{ "resource_id": "fal", "display_name": "Fal", "unit": "per megapixel",
"live_offers": 1, "best_discount_pct": 35, "median_discount_pct": 35 },
{ "resource_id": "wavespeed", "display_name": "Wavespeed AI", "unit": "per image",
"live_offers": 1, "best_discount_pct": 50, "median_discount_pct": 50 },
{ "resource_id": "elevenlabs", "display_name": "ElevenLabs", "unit": "per 1k characters",
"live_offers": 1, "best_discount_pct": 50, "median_discount_pct": 50 }
]
}best_discount_pct is the deepest live offer and median_discount_pct is the middle one — the pair tells you
whether that price survives one seller pausing. Pricing is the full treatment.
3. Make the call
The path after the provider id is the provider's own path. Two shapes, and getting this wrong is the single most common first mistake:
Fal's SDK appends the model itself, so a URL that already carries the path is refused unexpected_path before
anything else happens. Fal, end to end covers the SDK wiring.
# A one-second sound effect. Answers with audio bytes, not JSON. curl -X POST $SURPLUS_BASE_URL/resources/elevenlabs/v1/sound-generation \ -H "Authorization: Bearer $SURPLUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"text":"a short soft chime","duration_seconds":1}' \ --output chime.mp3 --dump-header -
# A queued image job. Answers at once with an id, not the image. curl -X POST $SURPLUS_BASE_URL/resources/wavespeed/api/v3/wavespeed-ai/flux-schnell \ -H "Authorization: Bearer $SURPLUS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"prompt":"a tiny blue paper boat on a white table","size":"1024*1024"}'
# EMPTY path. The target is a header, and it must be one of Fal's two model hosts. curl -X POST $SURPLUS_BASE_URL/resources/fal \ -H "Authorization: Bearer $SURPLUS_API_KEY" \ -H "Content-Type: application/json" \ -H "x-fal-target-url: https://fal.run/fal-ai/flux/schnell" \ -d '{"prompt":"a tiny red origami crane","image_size":"square","num_images":1}'
Idempotency-Key is accepted on every write, but a provider-key relay is not replay-safe by design: a
repeat is a second real generation at the provider and is billed as one. See
jobs and settlement.
4. Read what it cost
Every response carries both numbers, on every rail:
x-si-buyer-cost-microintegerWhat you were charged, in microdollars.
1 USD = 1000000.x-si-list-cost-microintegerWhat the provider would have billed for the same call, direct.
The difference is what the key market saved you. Both headers are CORS-exposed, so a browser client can read them too.
x-si-buyer-cost-micro: 1500 x-si-list-cost-micro: 3000
That call cost $0.0015 against a $0.003 list price — the seller's 50% off, applied to the provider's own quote rather than to a rate card of ours.
5. Jobs do not finish in step 4
Wavespeed predictions and Fal queue submits answer immediately with an id, not a result. The money follows the result, not the submit: the submit leaves the charge owed at the quote, and it settles at the actual figure when you fetch the result.
- SubmitHold placed at the quote
- BoundJob pinned to that one offer, 27 h
- Poll statusFree — no charge for asking
- Fetch resultCharged the actual, hold released
Jobs and settlement is the whole mechanism, including the follow-up URL rewriting that keeps your polls on your own key.
Requiring something of the account
Some sellers advertise properties of the account behind their key — zero retention, an EU region, a business plan. Require any of them per call:
curl -X POST $SURPLUS_BASE_URL/resources/elevenlabs/v1/sound-generation \ -H "Authorization: Bearer $SURPLUS_API_KEY" \ -H "X-SI-Require: data.zdr,residency.eu" \ -d '{"text":"a short soft chime","duration_seconds":1}'
A call that requires something only reaches offers claiming all of it. If none can, it is refused
trait_unavailable rather than quietly served by an offer that does not qualify — which is the point, and also
why requiring something can raise your price. GET /v1/resource/traits lists what is requirable, and
Provider Keys explains what each badge is actually worth.
Routing only to deep discounts
The min{N} mirror works here exactly as it does for inference — prefix the path and only offers at least N%
off are considered:
curl -X POST https://api.surplusintelligence.ai/min40/v1/resources/wavespeed/api/v3/wavespeed-ai/flux-schnell \ -H "Authorization: Bearer $SURPLUS_API_KEY" \ -d '{"prompt":"a tiny blue paper boat","size":"1024*1024"}'
If nothing is that cheap you get no_resource_offer rather than a worse price.
Minimum-discount routing has the details.
When a call does not land
Each refusal names one cause, so you can tell whose problem it is:
| Code | Status | What to do |
|---|---|---|
unexpected_path | 400 | A header-style provider (Fal) was given a path. Empty it. |
platform_route_refused | 403 | Never reachable, for any seller. Not a pricing or availability problem. |
policy_refused | 403 | Offers exist; no seller's rules allow this call. |
no_resource_offer | 404 | Nobody lists this provider, or none is this cheap. |
job_not_found | 404 | The job is not yours, or its binding expired after 27 h. |
seller_cap_reached | 429 | Every capable offer spent its daily cap. Resets 00:00 UTC. Retryable. |
offers_unhealthy | 503 | Every capable offer is backed off after failures. Retryable. |
provider_unpriceable | 400 | We cannot price it, so we refuse rather than guess. Use a priced model. |
trait_unavailable | 404 | Nobody advertises what you required. Retryable. |
insufficient_credit | 402 | Add prepaid credit. Nothing was reserved. |
unexpected_pathStatus · 400What to do · A header-style provider (Fal) was given a path. Empty it.platform_route_refusedStatus · 403What to do · Never reachable, for any seller. Not a pricing or availability problem.policy_refusedStatus · 403What to do · Offers exist; no seller's rules allow this call.no_resource_offerStatus · 404What to do · Nobody lists this provider, or none is this cheap.job_not_foundStatus · 404What to do · The job is not yours, or its binding expired after 27 h.seller_cap_reachedStatus · 429What to do · Every capable offer spent its daily cap. Resets 00:00 UTC. Retryable.offers_unhealthyStatus · 503What to do · Every capable offer is backed off after failures. Retryable.provider_unpriceableStatus · 400What to do · We cannot price it, so we refuse rather than guess. Use a priced model.trait_unavailableStatus · 404What to do · Nobody advertises what you required. Retryable.insufficient_creditStatus · 402What to do · Add prepaid credit. Nothing was reserved.Related
- Fal, ElevenLabs, Wavespeed — each provider's exact routes.
- Jobs and settlement — async jobs, bindings and when money moves.
- Provider key endpoints — the endpoint reference for this whole surface.
- Access rules — why a particular call is or is not reachable.
- Listing your key — the other side of the market.