Price guard: refuse a 30-second Seedance 2.5 render over your cap
Compute the Sume price of a clip from model, resolution and seconds before you POST, and refuse over a cap. Python code, the rates, and the 1080p cent rounding.

You can refuse an expensive render before you send it by computing the clip's Sume price from the model, the resolution, and the seconds, and comparing it with a cap you set. A 30-second Seedance 2.5 clip is $8.07 at 480p, $17.34 at 720p, and $42.65 at 1080p, so a guard that rejects anything over, say, $20 stops the 1080p mistake automatically.
The guard is yours, not Sume's. It is a cheap local check that sits in front of the paid call, and Sume's own reservation at submit remains the authority.
The rates to encode
Sume bills provider list times 1.25. Wan 3.0 lists at $0.05, $0.10 and $0.20 per second, which makes $0.0625, $0.125 and $0.25 per second on Sume. Seedance 2.5 is billed by video tokens in the pricing tables, and the per-second rates that reproduce the Sume cents at 4, 10, 15 and 30 seconds are about $0.269 at 480p, $0.578 at 720p, and $1.42155 at 1080p, rounded up to the cent.
| Model | Resolution | 10 s | 30 s |
|---|---|---|---|
| Seedance 2.5 | 480p | $2.69 | $8.07 |
| Seedance 2.5 | 720p | $5.78 | $17.34 |
| Seedance 2.5 | 1080p | $14.22 | $42.65 |
| Wan 3.0 | 480p | $0.625 | $1.875 |
| Wan 3.0 | 720p | $1.25 | $3.75 |
| Wan 3.0 | 1080p | $2.50 | $7.50 |
The guard
This standard-library function raises before any network call. The Wan rows are exact; the Seedance rows are tuned to match the published cents, so treat the table as a cache that you re-check against the pricing tables when rates change.
import math
RATE = { # USD per second on Sume, list x 1.25
("wan-3.0", "480p"): 0.0625, ("wan-3.0", "720p"): 0.125,
("wan-3.0", "1080p"): 0.25,
("seedance-2.5", "480p"): 0.269, ("seedance-2.5", "720p"): 0.578,
("seedance-2.5", "1080p"): 1.42155,
}
LIMITS = {"seedance-2.5": (4, 30), "wan-3.0": (2, 30)}
def price(model, res, seconds):
lo, hi = LIMITS[model]
if not lo <= seconds <= hi:
raise ValueError(f"{model} takes {lo}-{hi} s")
return math.ceil(RATE[(model, res)] * seconds * 100 - 1e-9) / 100
def guard(model, res, seconds, cap_usd):
p = price(model, res, seconds)
if p > cap_usd:
raise SystemExit(f"refused: ${p:.2f} > cap ${cap_usd:.2f}")
return p
print(guard("seedance-2.5", "720p", 30, 20.0)) # 17.34
print(guard("wan-3.0", "1080p", 30, 20.0)) # 7.5Why a guard at all
A copied prompt with the wrong resolution is the usual cause. The same 30 seconds is $8.07 or $42.65 on the same model, a factor of more than five. Put the cap in config, log every refusal, and let a person raise it deliberately.
Pair it with GET /v1/models style discovery: read supported_durations and supported_resolutions from GET /v1/videos/models so that the guard also rejects a 31-second request. Seedance 2.5 accepts 4 to 30 seconds and Wan 3.0 accepts 2 to 30.
What the guard cannot know
It does not know your balance, your plan's queue, or later rate changes. After the job, usage.cost on the poll response is the Sume billable amount, which is the number to reconcile your estimate against.
Where to keep the numbers
Hard-coded rates age. Treat the dictionary as a seed and refresh it from the video models listing, which returns prices alongside capabilities, whenever your deploy runs. A mismatch between your table and the listing should fail a test, not silently pass a bad estimate.
Also keep the cap per environment. A cap of $20 is sensible for a human trying a prompt and wrong for a nightly batch; the batch should have its own total budget, checked before the first submit.
Sources
Related posts
More in Developers
- Product photo to a 9:16 Reel clip in Python: 6 seconds for $0.75
POST /v1/videos with one product photo as frame_images first_frame on gemini-omni-flash-1.1: 6 seconds at 720p costs $0.75. Python polls and saves the MP4.
- Prometheus histogram buckets for Sume video jobs, up to 20 minutes
Pick histogram buckets for submit-to-terminal time on 30-second Seedance 2.5 and other Sume video jobs, so the 20-minute SDK deadline is the last bucket.
- Promise.allSettled for a wave of Sume jobs: keep the partial wins
Promise.all throws away nine good Sume videos when one job fails. Use Promise.allSettled over a wave sized to your plan's concurrency, in 30 lines of Node.
- provider_submission_failed 502: Sume could not start the job, retry
provider_submission_failed is a 502 meaning the job never started. By default it is retryable with retry_after_seconds 30 and next_action retry_later.
Written by Sume