Strategy

    Enforcing a spend ceiling in your dispatcher

    Budget enforcement belongs in your dispatcher. Price planned work against live balance and a ceiling you define, then refuse to submit past it.

    Versely Team7 min read

    Versely will not stop a key at N credits. A credit_limit field exists on the key row and is echoed when you create or list a key. Nothing in the request path reads it. Per-key RPM caps how often that key may call, not how many credits those calls spend. The account-level fuse is Insufficient credits at zero or at charge time. If you need "this worker may spend 2,000 credits this week and then stop," that number lives in your dispatcher.

    The pattern is short. Price the planned work. Read the live balance. Compare both against a ceiling you store. Refuse to POST /generate when either check fails. Pricing and the credits page are the units. The agent already does a version of this with estimate_cost and check_credits; estimating before you dispatch a batch and checking credits before generating cover that surface. This post is the REST version you run when the caller is a worker, not a chat.

    Why the key cannot be the ceiling

    Three controls people confuse:

    Control What it actually does What it does not do
    rate_limit_rpm on the key Caps requests per 60-second window (default 60, 1–1000) Caps credits. Five expensive videos per minute still spend.
    credit_limit on the key Stored, returned in create/list Enforced. Never read at auth or at charge.
    Profile balance 403 when credits <= 0; 402 when this job does not fit Implement your budget. A 50,000-credit wallet will happily run one worker down to empty.

    The 403/402 pair is a safety net for the account, not a job budget. A generate worker, a poster, and a human in the pipeline share one balance. Without a dispatcher ceiling, the worker can spend the poster's week. Tracking credits per client is how you explain it after the fact. The ceiling is how you prevent it.

    There is no customer webhook to tell you a job finished, and no Idempotency-Key to make a retry safe. You poll GET /api/v1/status/:requestId. Treat a successful submit as spend against the ceiling. Do not increment on a 429, 401, or 403 that never accepted the work. Do not decrement on failed unless the product credit history shows the charge did not stick. Guessing is how a "2,000 credit" cap becomes 3,100.

    The pre-flight

    You need two numbers from Versely and one from yourself.

    Live balance. GET /api/v1/user/me with the worker's key (read scope, because /user is a read prefix). The body includes user.credits. That is the account, not the job.

    Planned cost. POST /api/v1/ai-models/calculate-credits with { models, contentType, duration, resolution, count } for the selection you are about to send. Send the same Authorization header you will send to /generate, so the estimate uses the same pricing function as the charge. models is a non-empty array of names; contentType is required. Pass the real duration, resolution, and count you will generate with. Video and speech move with duration. Images are a per-generation charge. If you omit duration on a 20-second clip, you are not pricing the clip you will submit.

    The quote is a credit total for that selection, not a second, softer price list. If the body also includes a higher worst-case figure (full price when a discounted path is not used), gate the ceiling on the higher number. A dispatcher that only looks at the happy-path total will go through the ceiling on the path the charge already planned for.

    Your ceiling. An integer you store next to the job: 2_000 credits this week for nightly-hooks, 400 this run for a batch, 150 for a client's first draft. It is not a Versely setting. A spreadsheet is enough. A Redis key named spend:nightly-hooks:2026-W34 is better if more than one process submits.

    Worked numbers for a 30-second ad, if you want a sense of scale before you pick the integer, are on what a finished 30-second ad costs. Do not copy that figure in as the ceiling. Copy the method: price the actual model and duration.

    Refuse, then submit, then count

    Pseudocode for one worker:

    ceiling = 2000          # this job, this period
    spent   = load(spent)   # what this dispatcher has already submitted
    
    balance = GET /api/v1/user/me -> user.credits
    quote   = POST /api/v1/ai-models/calculate-credits { models, contentType, duration, resolution, count }
    need    = worst-case credits from quote  # the higher number if two are returned
    
    if spent + need > ceiling:
        refuse("job ceiling")      # your error, not Versely's
        stop
    if need > balance:
        refuse("account balance")  # would 402/403 anyway
        stop
    
    res = POST /api/v1/generate/video { model, prompt, duration, ... }
    if res accepted with request_id:
        spent += need
        save(spent)
        poll GET /api/v1/status/:requestId
    else if 429 / 401 / 403 scope:
        do not increment spent     # request was not accepted
    else if 402 / 403 insufficient credits:
        do not increment spent
        stop the run
    

    Rules that keep the arithmetic honest:

    1. Count on accept, not on complete. A generating job has already been accepted and will charge. If you wait for completed to increment spent, two overlapping submits will both pass the ceiling check.

    2. Do not double-count a retry. There is no idempotency key. A second POST is a second job. If you retry a generate, add it again or do not retry.

    3. Poll; do not guess completion. GET /api/v1/status/:requestId returns generating | completed | failed. A failed with transient: true is the only status-shaped hint that a resubmit might be warranted. Resubmit only if the ceiling still has room.

    4. Reconcile against credit history in the product. /billing is the ledger group; status payloads do not include a receipt. Your spent is an estimate of what this job submitted. The history is what the account actually moved. Drift is either a refund you did not notice or a submit you did not record. Fix the ledger; do not raise the ceiling to match the drift.

    5. One ceiling per key, one key per job. A shared "all" key with one ceiling is an account cap you built yourself, badly. Nightly hooks get a hooks ceiling. The poster gets a poster ceiling. The human in the web app is not in this dispatcher, so leave them headroom on the live balance check.

    The agent equivalent, if the "dispatcher" is a chat turn rather than a worker:

    • estimate_cost takes items (image, video, music, speech, or sound effect, with optional model, count, and duration_sec) and returns per-item credits, a total, and the current balance. Same pricing brain as a real charge.
    • check_credits takes operation and estimated_credits and is the yes/no balance gate before a multi-step run.

    Those tools are lookups. They do not enforce a ceiling either. The refuse step is still yours: if the estimate plus the period spend exceeds the number you chose, or if check_credits says the balance cannot cover the job, you do not call generate.

    FAQ

    Can I set credit_limit on create and have Versely stop the key?

    You can send it. It is stored and returned. No middleware and no charge path reads req.apiKey.creditLimit. A key with credit_limit: 100 will keep generating until the account cannot pay. Put the 100 in your dispatcher.

    Should I gate on the discounted total or the higher figure?

    The higher figure, when the quote includes both. That is the worst case if a discounted path is not used. Gating on the happy-path number is how a ceiling of 2,000 is breached without any bug in the increment.

    Does calculate-credits cost credits?

    No. It is a pricing lookup. Calling it before every submit is the correct default. Sending the same Bearer token you will generate with keeps the quote in the same context as the charge.

    What if two processes share the ceiling?

    Then spent cannot live in a process-local variable. Use a single store both writers increment atomically before POST, and decrement only if the POST was rejected before accept. Two workers that both read spent = 1,900 and both submit a 200-credit job will go through a 2,000 ceiling. That is a locking bug in your dispatcher, not a missing Versely flag.