Budgets & Cursors
Two parameters exist for automated callers: max_credits caps what a single
call is allowed to use, 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
A few endpoints use usage per result, so the final draw depends on how much
data comes back. max_credits is an optional legacy ceiling you can state up
front on those calls.
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 exceed your ceiling, no work is done,
nothing is charged, and you get a 400:
{
"error": "This call could cost more than your max_credits ceiling. Nothing was charged and no work was done.",
"error_code": "max_credits_exceeded",
"max_credits": 200,
"hint": "Lower limit, drop classify=true, or raise max_credits."
}The check runs against the maximum the call could use, before any billable
work happens. You are still charged only for what was actually computed, and
sections that are free when empty (like include=founders) stay free.
Where it applies
| Endpoint | What scales usage |
|---|---|
GET /v1/trending | classify=true, per entity returned |
POST /v1/classify | per entity found |
GET /v1/search?scope=catalog | include_founders=true, per company that resolves |
POST /v1/founders/batch | per company that resolves |
GET /v1/company/:handle | each include= section |
POST /v1/track (batch) | per account new to the platform |
Every other endpoint draws a fixed amount, so max_credits is not needed and is
ignored. Most callers skip it and watch usage_pct instead.
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.