Budgets & Cursors
Two parameters exist for automated callers: max_credits caps what a single
call is allowed to cost, and cursor makes a repeating job idempotent. Both
are optional and additive. Nothing changes for callers that ignore them.
max_credits - cap the cost of one call
Most endpoints are flat-priced, but a few price per result. On those, the final cost depends on how much data came back, which an agent could not know until after it was charged.
max_credits is the ceiling you state up front.
curl "https://frontrun.vc/v1/trending?limit=100&classify=true&max_credits=200" \
-H "X-API-Key: your_api_key"It fails closed. If the call could cost more than your ceiling, no work is
done, nothing is charged, and you get a 400 telling you the number:
{
"error": "This call could cost up to 1624 credits, which is above your max_credits of 200. Nothing was charged and no work was done.",
"error_code": "max_credits_exceeded",
"max_cost_credits": 1624,
"max_credits": 200,
"max_cost_breakdown_credits": { "trending": 24, "classification": 1600 },
"charged_credits": 0,
"balance_credits": 9836,
"hint": "Classification is 16 credits per entity returned. Lower limit (currently 100), drop classify=true, or raise max_credits."
}The ceiling is checked against the worst case
The check runs against the maximum the call could cost, before any billable work happens. Billing itself is unchanged: you are still charged only for what was actually computed. A call that clears the ceiling usually lands well under it.
That matters most on the sections that are free when empty. include=founders
could cost 100 credits, so it counts 100 toward the ceiling, but it costs 0 if
no founders are found.
Reading the estimate without a failed call
Every endpoint that honors max_credits returns max_cost_credits in its
normal 200 response. That is the same worst-case number, for the parameters
you sent, so you can budget the next call from the last one.
Where it applies
| Endpoint | What scales the cost |
|---|---|
GET /v1/trending | classify=true adds 16 credits per entity returned |
POST /v1/classify | 16 credits per entity found |
GET /v1/search?scope=catalog | include_founders=true adds 100 credits per company that resolves |
POST /v1/founders/batch | 100 credits per company that resolves |
GET /v1/company/:handle | each include= section is priced at its standalone rate |
POST /v1/track (batch) | 4 credits per account new to the platform |
On every other endpoint the cost is a fixed number you can read from
Pricing, so max_credits is not needed and is
ignored.
cursor - resume exactly where the last run stopped
since describes a window. It cannot tell you what you have already
processed, so a job that fails and retries either reprocesses its overlap or
guesses at a gap.
A cursor is an opaque watermark. Read a page, process it, save next_cursor,
and send it back on the next run.
# First run: use since as normal.
curl "https://frontrun.vc/v1/follows/new?since=24h" -H "X-API-Key: $KEY"
# -> { ..., "next_cursor": "frc1_eyJ2IjoxLCJ..." }
# Every run after that: no window to guess at.
curl "https://frontrun.vc/v1/follows/new?cursor=frc1_eyJ2IjoxLCJ..." \
-H "X-API-Key: $KEY"Rules
- Persist
next_cursoronly after you have successfully processed the response. If your job dies mid-batch, retrying with the old cursor returns the same window again. That is the point. - Replaying the same cursor is safe and returns the same window.
- Cursors are opaque. Do not parse, edit, or construct them. A modified token is rejected.
- Cursors are scoped to the endpoint, the API key, and the filters you sent
(
username,sector,event_type, and so on). Change a filter and mint a fresh cursor withsince, otherwise the records your old filter excluded would be skipped forever. Replaying a cursor across a different scope returns a400instead of silently doing the wrong thing. cursorandsinceare mutually exclusive in effect. Whencursoris present it sets the window andsinceis ignored.
has_more: true with next_cursor: null is not a bug
The cursor marks the end of the whole matched window, not the end of the
page you were handed. If we issued it mid-pagination, your next run would
start after pages you never fetched, and those records would be skipped
forever. So while has_more is true, next_cursor is null on purpose and
the response tells you the exact next call in next_offset.
The loop, concretely. Say the window matched 77 groups and you received 50:
{
"total": 77,
"returned": 50,
"offset": 0,
"has_more": true,
"next_cursor": null,
"next_offset": 50,
"next_cursor_note": "next_cursor is only issued on the final page..."
}- Call again with
?offset=50(the value innext_offset). You get the remaining 27. - That page has
has_more: falseand a realnext_cursor. - Persist it after processing, and send it as
?cursor=on the next run.
Or avoid the loop entirely: raise limit (up to 200) so the window fits in
one page and the first response carries the cursor.
Granularity
Follow data is stored as one snapshot per tracked account per day, so the watermark is a snapshot boundary. A cursor resumes at the first snapshot after the newest one your last response consumed. Nothing is delivered twice and nothing between the two runs is missed.
One exception worth knowing: accounts still establishing a baseline are
answered from a live check against X rather than from a snapshot. Those results
carry live_refreshed: true and can appear once more in the following window,
once the poller writes them into a snapshot. Dedupe on twitter_user_id if
that matters to you.
Where it applies
| Endpoint | Watermark |
|---|---|
GET /v1/follows/new | newest follow snapshot consumed |
GET /v1/follows/enriched | newest follow snapshot consumed |
GET /v1/feed | newest follow snapshot consumed |
GET /v1/reports | newest report_date returned |
/v1/trending and /v1/convergence deliberately do not take a cursor.
They are ranked aggregations over a window, not record listings: "top 25
trending in the last four hours" is not the continuation of "top 25 in the last
seven days", so a watermark there would quietly degrade the ranking rather than
paginate it. Keep using since for those.