Skip to main content
MarketplaceProviders

Providers

The three live providers, one section each: Fal, ElevenLabs and Wavespeed.

Fal

Fal is the one provider on the key market that does not take its target in the URL. That single difference accounts for most first-call failures, so it is the first thing in this section.

Billing unit: per megapixel. Fal keeps no record of a call against the seller's account, so nothing you generate here persists on someone else's history.

The target is a header, and the path is empty

Why Fal is shaped differently
What worksPOST /v1/resources/fal + x-fal-target-url: https://fal.run/fal-ai/flux/schnell
What is refusedPOST /v1/resources/fal/fal-ai/flux/schnell → 400 unexpected_path
Fal's own SDK builds the full model URL and expects to send it through a proxy as a header. A path on our route would be a second, conflicting target — so it is refused outright rather than guessed at.

The target must be an absolute https URL on one of Fal's two model hosts. Anything else — Fal's storage, pricing or account hosts, or any other domain — never leaves our fence.

fal.runWhat it is · Answers with the result itself. Synchronous.
queue.fal.runWhat it is · Starts a job and answers with an id.

Using the SDK

@fal-ai/client is the normal way in. Point proxyUrl at our route and send the target always, not just when the SDK feels like it:

Fal SDK through Surplus
import { fal } from '@fal-ai/client';

fal.config({
  // No Fal key here — the seller's key is resolved on our side, at call time.
  proxyUrl: {
    url: '$SURPLUS_BASE_URL/resources/fal',
    when: 'always',
  },
  requestMiddleware: async (request) => ({
    ...request,
    headers: { ...request.headers, Authorization: `Bearer ${process.env.SURPLUS_API_KEY}` },
  }),
});

const result = await fal.subscribe('fal-ai/flux/schnell', {
  input: { prompt: 'a tiny red origami crane', image_size: 'square', num_images: 1 },
});
console.log(result.data.images[0].url);

when: 'always' matters. With the default the SDK proxies only some calls and sends the rest straight to Fal, where your request has no credential and fails — with nothing on our side to explain why.

Fal is the one provider whose follow-up URLs we leave untouched: its SDK builds them itself and sends each target in the header, so it stays on your key without our help. Path-style providers get their follow-up URLs rewritten instead — see jobs and settlement.

A worked queue job

Submit, poll, collect
curl -X POST $SURPLUS_BASE_URL/resources/fal \
  -H "Authorization: Bearer $SURPLUS_API_KEY" \
  -H "x-fal-target-url: https://queue.fal.run/fal-ai/flux/dev" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a paper boat","image_size":"square"}'
# → { "request_id": "764cabcf-…", "status_url": "…", "response_url": "…" }

COMPLETED does not mean it worked. A failed Fal job also reads COMPLETED, with no error on the status, so the outcome is only confirmed on the result: 2xx succeeded, 4xx failed. We settle on the same evidence you do.

What is reachable

Only Fal's own public models, on three methods:

POST /fal-ai/+Covers · Submit or run a model.
GET /fal-ai/+Covers · Status and result.
PUT /fal-ai/+Covers · Cancel.

fal-ai is the only publisher. That is not a preference — a seller's private apps live under <their-username>/<app> and answer on these same two hosts to the seller's key, so no denylist could cover them, while nobody else can deploy under fal-ai. An allowlist of one publisher is the only shape that is safe by construction. Other public publishers are added as data once verified.

?logs=1 is the one query parameter passed through, because the SDK asks for run logs on status polls. fal_webhook is deliberately not: it would send your result to a URL we do not control.

Two response headers come back untouched, because the SDK reads both: x-fal-request-id and x-fal-request-timeout-type.

Two things Fal refuses here.

LLMs

fal-ai/any-llm is Fal's unified LLM endpoint — Claude, GPT, Gemini, Llama — and it is denied provider-wide, not per listing.

An LLM bills on output tokens, which no quote taken before the call can bound. That makes it a platform pricing gap rather than a seller's choice, which is exactly why it is not left to the access editor. The rule is unqualified across every method, not POST-only: a family refused on one verb and open on the others reads as an oversight the next time someone edits it.

LLM resale is the order book's job, not this market's. Use /v1/chat/completions.

Models that can run for minutes

Fal prices most image models per megapixel or per image, and those answer inside the request. Everything else — video, 3D, long audio — can run for minutes.

Why a long model must be queued
  1. Priced per megapixel or imageRuns on fal.run, answers inline
  2. Priced any other wayRefused on fal.run — 400 job_route_required
  3. Submit to queue.fal.runSurvives a replaced task
A synchronous call holds a task until the provider answers, and a task being replaced by a deploy drains for 60 seconds. A minutes-long model cut there is billed to the seller and never reaches you — so it is refused up front instead.

The refusal names itself: job_route_required. Submit the same model to queue.fal.run instead and follow it as a job. A queue submit answers immediately either way, so this costs you nothing but a different target host.

On the x402 rail there is no queue option — the message says so, and you need a Surplus API key for those models.

Timeouts

POST /fal-ai/+ is capped at 50 seconds, inside the fleet's 60-second drain, so a deploy never cuts a call a seller is already billed for. A model that needs longer belongs on the queue.

ElevenLabs

ElevenLabs is a path-style provider: the provider's own path goes straight onto our route, and the seller's xi-api-key is attached on our side.

Billing unit: per 1 000 characters.

ElevenLabs is the one live provider where the seller's account keeps a record of your call. Their generation history shows that a request happened, even on routes where we delete the result after handing it to you. That is a property of the provider, not of our proxy, and it is why the trait badges on a listing matter here more than anywhere else.

Three routes, and that is the whole surface

ElevenLabs publishes over four hundred operations. Three are reachable.

What the allowlist admits
POST /v1/sound-generationa short sound effect — your result, nothing of the accountopen
POST /v1/musicleaves no history; its two account fields are refused where it is pricedopen
GET /v1/modelsthe static model list, free and read-onlyopen
everything elseaccount data, stored data, or cannot be priced before the callrefused
An allowlist, not a denylist: a route is unreachable until someone opens it deliberately. Everything not named here is refused, including routes nobody has thought about yet.

?output_format= is the one query parameter passed through — it is the only one the three routes take.

Calling them

ElevenLabs through Surplus
# Answers with audio BYTES, not JSON. The cost headers still ride on the response.
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 headers.txt

grep x-si- headers.txt
# x-si-buyer-cost-micro: 1500
# x-si-list-cost-micro: 3000

POST /v1/music is capped at 50 seconds, inside the fleet's 60-second drain, so a deploy never cuts a generation the seller is already billed for.

Why the other routes are closed

The rule is one sentence: the seller's account may keep a record of your request, but you may read nothing of that account. Each family below fails it for a different reason, and the reasons are worth knowing because they tell you what would have to change.

Can they…Reaches account dataUnpriceable up frontLeaves seller history
Text-to-speech, speech-to-speech, dialogueYesvoice_id may be a private cloned voiceNoYes
Speech-to-textYesuse_speaker_library, webhookNoYes
The voice lists (/v1/voices, /v2/voices)Yesthey ARE the seller’s libraryNoNo
Audio isolationNoYesbilled by uploaded audio lengthYes
Forced alignmentNoYessame basis as isolationYes
Account, keys, billing, usageYesservice accounts can mint keysNoNo
StreamingNoYescannot be metered for settlementNo
Read across a row for why that family is closed. Two different disqualifiers recur: a field that reaches the seller's own account data, and a billing basis no quote taken before the call can bound.

Two of those are worth drawing out:

  • Voice routes are not a capacity question. A voice_id can name one of the seller's private cloned voices, so serving text-to-speech would need an ownership check we do not have yet. Opening it is that check, not a policy edit.
  • Audio isolation and forced alignment are priced by the length of a file you upload. No quote taken before the call can bound that, which is the same property that keeps LLMs off Fal. Opening either is a cost-source change first.

Music is the exception that proves the rule: it leaves no history, and its two fields that would touch the account — finetune_id and store_for_inpainting — are refused at the point where it is priced.

Two layers, not one

A route can be closed two ways here, and the difference is deliberate.

Everything ElevenLabs publishesover 400 operations
Named on the denylistaccount, stored data, voices, streams — can never be opened by widening an allow rule
Refused by absence alonenot opened yet; e.g. the audio-effects catalogue
Opensound generation, music, models
The allowlist is what we have opened. The denylist underneath is the safety net: it names the routes that reach account data, so a later, wider allow rule still cannot open them.

GET /v1/audio-effects/catalog is deliberately not on the denylist. It lists filter presets — a static catalogue of the same class as GET /v1/models, which is open — so naming it would imply it reaches the seller's account, which it does not. It stays refused by the allowlist, which is what "we have not opened this yet" looks like. A denylist that collects everything unopened stops telling you which entries are load-bearing.

Wavespeed

Wavespeed is a path-style provider, and almost everything on it is a job: you submit a task, you get an id, and you collect the result separately.

Billing unit: per image. Wavespeed keeps no record of a call against the seller's account.

What is reachable

Two rules, and together they are the whole surface:

POST /api/v3/{publisher}/+Covers · Submit a task to one of 44 allowed publishers' models.
GET /api/v3/predictions/*/resultCovers · Read one task's result.

Generation only. No price lookups, model lists, uploads, balance, billing or prediction history — and POST /api/v3/predictions, the generic submit, is refused precisely because it would sidestep the per-publisher allowlist.

The 44 publishers are the model owners Wavespeed hosts:

alibaba  black-forest-labs  bria  bytedance  character-ai  chatterbox
clarity-ai  decart  elevenlabs  google  heygen  hitem3d
hyper3d  ideogram-ai  inworld  kwaivgi  leonardoai  lightricks
luma  meshy  meta  microsoft  midjourney  minimax
mirelo-ai  mureka-ai  nvidia  openai  pika  pixverse
pruna-ai  recraft-ai  runwayml  skywork-ai  sonilo  sourceful
sync  topaz  tripo3d  veed  vidu  wavespeed-ai
x-ai  z-ai

A model under any other owner is refused policy_refused — the publisher is part of the rule, not a hint.

A listed publisher does not mean every model under it is priced. black-forest-labs/flux-schnell reads like the obvious first call and is a 404 at Wavespeed's own price endpoint, so it is refused provider_unpriceable rather than charged at a guess. wavespeed-ai/flux-schnell is the priced equivalent. If a model refuses this way, try the wavespeed-ai mirror of it.

Submit, then collect

A Wavespeed task, end to end
  1. POST the modelAnswers { data: { id, urls } }
  2. Bound to that offer27 h, your account only
  3. GET the status URLFree. created → processing → completed
  4. GET …/resultThe image, and the charge settles
The submit answers at once — the id is not the image. The charge is owed at the quote from the submit and settles at the actual figure when you read the result, so a task you never collect is never charged at an actual.
A full task
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"}'

Look closely at urls.get in that response. Wavespeed returned its own api.wavespeed.ai URL; we rewrote it to point back at our route before handing it to you. Follow it as given and your poll stays on your own Surplus key and on the same seller's offer. Call Wavespeed's URL directly and you have no credential.

That rewriting is why path-style providers need no special handling from you, and it is covered in full under jobs and settlement.

Streaming is refused

Wavespeed publishes streaming variants at /api/v3/{publisher}/{model}/stream, and they are denied provider-wide rather than left to each seller.

A streamed response has to be metered from the stream, which this rail does not do — it would be billed at the submit's estimate and settle wrong. That makes it a platform capability gap, not a seller's choice. Both the /stream and /stream/* forms are denied, because a variant may nest.

Two layers here as well

Everything Wavespeed publishes
Named on the denylistbalance, billing, user, webhook secret, media uploads, prediction history, streams
Refused by absenceanything under an unlisted publisher
Open44 publishers’ models, and one result read
The allowlist admits 44 publishers and one result read. The denylist underneath names the account routes, so widening the allowlist to pick up a new publisher cannot accidentally open them.

/api/v3/webhook* is named deliberately: it holds the account's webhook signing secret. It was already refused by being off the allowlist, but a fence that only works while the allowlist stays narrow is not a fence.