Docs
Reference
Budgets & Cursors

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

EndpointWhat scales the cost
GET /v1/trendingclassify=true adds 16 credits per entity returned
POST /v1/classify16 credits per entity found
GET /v1/search?scope=cataloginclude_founders=true adds 100 credits per company that resolves
POST /v1/founders/batch100 credits per company that resolves
GET /v1/company/:handleeach 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_cursor only 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 with since, otherwise the records your old filter excluded would be skipped forever. Replaying a cursor across a different scope returns a 400 instead of silently doing the wrong thing.
  • cursor and since are mutually exclusive in effect. When cursor is present it sets the window and since is 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..."
}
  1. Call again with ?offset=50 (the value in next_offset). You get the remaining 27.
  2. That page has has_more: false and a real next_cursor.
  3. 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

EndpointWatermark
GET /v1/follows/newnewest follow snapshot consumed
GET /v1/follows/enrichednewest follow snapshot consumed
GET /v1/feednewest follow snapshot consumed
GET /v1/reportsnewest 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.