Unit price for a model chosen on a generic node's provider-model property (e.g.
model on nodetool.image.TextToImage). The editor's cost preview and the
server-side pre-run budget estimate both call getModelUnitPrice, so a run is
gated on the same number the editor shows.
import { getModelUnitPrice } from "@nodetool-ai/model-pricing";
getModelUnitPrice({ id: "fal-ai/flux/schnell", provider: "fal_ai" });
// { unit_price: 0.003, billing_unit: "images", currency: "USD", source: "bundle" }Looked up in order, first hit wins:
- FAL —
@nodetool-ai/fal-nodes/unit-pricing-catalog, keyed byendpoint_id. - kie —
@nodetool-ai/kie-nodes/unit-pricing-catalog, keyed bymodel_id, using the USD conversion (a raw credit figure has no fixed USD value). - GenSpend —
src/generated/genspend-pricing.json, keyed<provider_id>:<model_id>.
FAL and kie come from the providers themselves, so they stay ahead of GenSpend. GenSpend covers every other provider NodeTool can run and GenSpend tracks — Replicate, AtlasCloud, Together, Gemini, OpenAI, MiniMax, ElevenLabs, xAI — plus any FAL or kie model their own catalogs predate.
All three are imported as modules, not read off disk, so the estimate works
identically in the browser bundle and inside the packaged Electron backend (no
PACKAGE_RUNTIME_ASSETS entry needed).
GenSpend tracks what image/video/audio models cost across
the providers that sell them, at GET /api/v1/models — public, read-only,
synced from providers every ~6 h or hand-verified weekly.
scripts/sync-genspend-pricing.mjs normalizes that catalog into the shipped
JSON:
npm run build:packages # the sync reads the built providers
npm run sync:genspend # rewrite the catalog
npm run sync:genspend:check # exit 1 if it is staleThe GenSpend Pricing Sync workflow runs it nightly and opens a PR when a price
moved. Never hand-edit the generated file.
GenSpend keys models by its own slug (seedance-2); a run is priced by the
provider-native id on the node (bytedance/seedance-2.0/text-to-video). Each
entry records which of five routes bridged them, in descending order of trust:
alias— pinned by hand inscripts/genspend/aliases.json. An array of ids pins a match the name comparison cannot see;nullblocks a model for that provider.variant— avariants[]row whosespecnames a model NodeTool ships. Best of the automatic routes, because the row carries that endpoint's own price:fal-ai/flux-pro/v1.1bills $0.04 and…/v1.1-ultra$0.06, and both get their real number instead of sharing the model's headline price.provider-id— GenSpend'sproviderModelId, the id the provider's own API invokes and bills with, on a receipt-or-null contract. Verified against the provider's NodeTool listing before use, so an id for a model NodeTool doesn't ship (resemble-ai/chatterbox) resolves to nothing instead of a bad key.receipt— the offering'ssourceUrlis a model page carrying the native id:fal.ai/models/fal-ai/flux/schnell,replicate.com/black-forest-labs/flux-dev. Ranks belowprovider-idbecause it infers the id from a URL path.catalog— the model's normalized name exactly equals that of a model the provider itself enumerates in NodeTool (getAvailableImageModelsand friends). That listing is the model picker's own source, so the sync can only ever emit ids NodeTool actually ships.
GenSpend prices a model once per provider, but a provider ships it as several
task endpoints and only one of those is what providerModelId or a receipt
names. So the name tier runs even when an exact id was found, carrying the price
to the model's sibling endpoints at catalog trust — capability-checked, so a
task the model cannot do gets nothing. AtlasCloud's Seedance 2.0 ends up with its
text-to-video endpoint priced exactly and image-to-video and reference-to-video
priced by inference, each labelled for what it is.
Five guards keep a wrong number out of a budget decision:
- Published capabilities are respected. GenSpend's receipted task flags
(
t2i/i2i/t2v/i2v/r2v/v2v) drop an endpoint whose task the model cannot do —qwen-imagereportsi2i: false, so its price never lands on kie's image-edit endpoint. Only an explicitfalseblocks;nullis unknown, not refuted. - Same modality, and speech is not music. "Gemini 3.1 Flash Image" and
"Gemini 3.1 Flash TTS" normalize alike once the task word is stripped; the
modality guard keeps them apart, and
capabilities.kind === "music"keeps a music model off a provider's text-to-speech listing. - Exact key equality, never prefix or fuzzy — so
seedance-2cannot priceseedance-2-mini, andkling-3-procannot pricekling-3-turbo. A model's publishedaliases[]are matched too, which is how Nano Banana Pro and Gemini 3 Pro Image find each other. - Generation endpoints only. Upscalers, lip-sync and voice-clone endpoints bill on their own basis and never inherit a model's generation price.
- Ambiguity is dropped, not resolved. A name hitting more than eight ids is a family name, not a model; it is reported instead of priced.
- A video-input variant never prices a text-to-video endpoint.
videoInput: trueis a billing axis, not a spec note — providers charge more when a video goes in, so applying such a row to a t2v endpoint would undercharge. The headline price stands instead. - Tier ambiguity resolves upward. Where a provider selects tier by parameter, Pro and Standard share one callable id, so one NodeTool node could run either. Two equally-trusted models on one id keep the dearer price; the same model listed twice keeps the cheaper, which is what a run actually pays.
Anything left unresolved is printed by the sync and written to its --report
file, so the gap is visible rather than silently absent.
The sync requests ?envelope=1 (so the payload carries its own generatedAt)
and sends the stored ETag as If-None-Match; a 304 ends the run as a no-op.
Two wrinkles, both handled:
- A compressing intermediary marks GenSpend's strong ETag weak in transit, and the origin then compares strongly and misses. The sync stores and sends the strong form.
- The ETag covers the envelope's
generatedAt, which turns over with the 60 s edge cache, so a nightly run usually gets a fresh body anyway.etagandcatalogGeneratedAtare therefore frozen alongside the prices — otherwise the shipped file would change every night and open a PR with no price movement.
unit_price is in the provider's own unit; prices are only comparable inside
one unitClass. Each entry keeps unit_class verbatim and maps it to a
billing_unit label ("images", "seconds", …) matching the FAL catalog's
vocabulary. Offerings that aren't available or carry no price are dropped.
When two offerings land on one key, a receipt match outranks a name match, and
otherwise the cheaper price wins.
Prices via genspend.io.