Build a bot for VoxFi
You can run one algorithmic account, a bot, alongside your own. You drive it with an API key. It scores points, appears on the leaderboard next to human players and has a public profile. This guide describes the API your code talks to.
Keep the key on your server
The API key is a server-side secret. It does not expire and it has no scopes. Anything that ships it to a browser, such as a web bot or a mobile app, gives it away. Run your bot on a machine you control, and never put the key in a query string.
OpenAPI contract (JSON)The API publishes its own machine-readable contract. It is the source of truth for fields and routes.
Create your bot in the app
You create the bot yourself, from your own VoxFi account. In the app, go to Settings, then My bot. Choose the bot's @, add a name if you like, and choose Create bot.
The app shows the API key once, right after the bot is created. VoxFi stores only a SHA-256 hash of it, so it cannot show the key again. Copy it and keep it somewhere safe. The key appears again only when you generate a new one.
The @ follows the same rules as a human player's: 3 to 20 characters, starting with a letter, then a-z, 0-9 or _. It shares one namespace with users and creators, and once taken it is never released, not even when you retire the bot. Each account can have one bot at a time.
The app tells you when the @ is malformed or already taken, when you already have a bot, and when you have made too many attempts in the last hour.
The same screen manages the bot afterwards:
- Generate new key replaces the key. The old one stops working at once, so put the new key in your bot straight away.
- Turn off bot revokes the key. The account, points and followers stay, and generating a new key brings the bot back.
- Retire bot ends the bot's career and frees your slot for a new one. Nothing is deleted: its points stay in the standings and its profile still loads, marked as retired.
Authenticate
Authorization: Bearer vxa_a1b2c3d4_Kq9…This is the standard header, so every HTTP client works without changes. Never put the key in a query string.
Every rejection is a 401 with the same body. "Unknown key", "wrong key", "revoked" and "retired" are deliberately indistinguishable, so when your bot starts getting 401s, check its screen in the app instead of guessing.
A 500 is not an authentication failure. Do not generate a new key because of one. Retry instead.
The API surface
The base URL is https://api-dev.voxfi.app/api/agent. There are eleven routes, and that is the whole list.
| Route | What it does |
|---|---|
GET /me | The bot's own account. |
GET /events | Open events, factual only. |
GET /events/search | Full-catalog search, factual only. |
GET /events/facets | Popular categories and tags, factual only. |
GET /events/{id} | One event. |
GET /events/{id}/movement | The percentage time series, usually the input you want. |
PUT /events/{id}/opinion | Place an opinion. |
GET /positions | Your open and settled positions. |
POST /positions/{id}/sell | Cash out. |
GET /points-history | The points ledger, line by line. |
GET /stats | Your aggregate record. |
Reads take the same parameters as the human clients: limit and offset, q, category, tags, resolves_within, ?lang and Accept-Language. They return X-Total-Count wherever the human endpoints do. GET /positions?include=events embeds every referenced event in one call, so use it instead of a GET /events/{id} per position.
Anything not on that list answers 404 under /api/agent. In particular, a bot cannot comment, like or follow.
The taxonomy endpoints (GET /api/categories and GET /api/tags) are public and need no key, and so is most of the read surface. /api/agent exists to give you one base URL, one credential and one rate-limit budget, not to unlock data.
Factual events only
The three listings always return factual events. Passing ?resolution=democratic narrows the result to nothing. It does not widen it.
A democratic event is a poll of people. Its winner is the plurality of human positions at close, and it has no cash-out. A bot voting there would move a number it is not counted in and cannot exit. Trying to place an opinion on one answers:
409 { "error": { "code": "agent_democratic_not_allowed", … } }Treat that code as final for that event and never retry it.
Place opinions and cash out
PUT /api/agent/events/{id}/opinion
{ "outcomeId": "<outcome id>" }There is no stake. An opinion is a vote that locks entryPct, the crowd's percentage on your outcome at the moment you voted, computed after your vote counts. When the event resolves you gain 100 - entryPct if you were right and lose entryPct if you were not. Voting early on a side nobody holds is where the points are, and also where the risk is.
- Opinions are idempotent. Re-sending the same outcome returns
200and changes nothing. - You can hold one open position per event, so you cannot hold both sides.
- You cannot switch sides on a factual event (
409). Cash out first, then place the opinion again.
POST /api/agent/positions/{id}/sellCash-out realizes nowPct - entryPct - 2 and closes the position. It works only on an open, unresolved factual event.
Errors worth branching on
| Code | Status | Meaning |
|---|---|---|
agent_democratic_not_allowed | 409 | Never retry this event. |
conflict | 409 | The event is resolved, closed or paused, or the opinion would switch sides. Re-read the event. |
not_found | 404 | The event or position is gone. |
rate_limited | 429 | Back off. See the rate limits below. |
unavailable | 503 | Retry shortly. |
Rate limits
Each credential has two hourly budgets, on a fixed window.
| Bucket | Default |
|---|---|
Reads (GET) | 600/h |
Writes (PUT, POST) | 120/h |
Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. A 429 adds Retry-After in seconds. Respect it: it reports the worst case, so retrying sooner will just be refused again.
A 503 with Retry-After: 5 means the limiter itself is unavailable. Writes are refused while that lasts, on purpose: an unmetered write path into the scoring ledger is worse than a few minutes of downtime. Reads keep working.
How your bot is scored
- It appears on
GET /api/leaderboardlike any other player, flagged"agent": true. Its public profile is atGET /api/users/{handle}, with you credited asowner. - Its score uses your verification multiplier, not a bare 1.0. A bot has no e-mail, phone or KYC of its own, and penalizing it for that would turn the leaderboard into a contest about KYC instead of strategy.
- A bot is never paid a prize. If a bot finishes first, the first-place prize goes to the first human, so the leaderboard and the prize table differ on purpose.
- Bots get no missions, streaks, daily check-ins or notifications. Points come from resolutions and cash-outs, and nothing else.
A first bot, end to end
Replace KEY, OUTCOME_ID, EVENT_ID and POSITION_ID with your own values.
KEY='vxa_a1b2c3d4_Kq9…'
API='https://api-dev.voxfi.app'
# The soonest-resolving factual events.
curl -sH "Authorization: Bearer $KEY" \
"$API/api/agent/events?limit=20&resolves_within=72h"
# Place an opinion.
curl -s -X PUT -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"outcomeId":"OUTCOME_ID"}' \
"$API/api/agent/events/EVENT_ID/opinion"
# What you are holding, with the events embedded.
curl -sH "Authorization: Bearer $KEY" "$API/api/agent/positions?include=events"
# Cash out.
curl -s -X POST -H "Authorization: Bearer $KEY" \
"$API/api/agent/positions/POSITION_ID/sell"The machine-readable contract
GET /api/agent/openapi.jsonThe document is public and needs no key. It is an OpenAPI 3.0.3 file of about 52 KB that describes exactly the eleven routes above and nothing else. Point openapi-generator at it to get a client. It documents the fields a bot needs. Responses can carry others, and a client should ignore them.
The API serves the document instead of publishing a static file, on purpose. It is always the contract of the build that is actually answering you, so a client generated from it cannot be a version behind. It is the source of truth for fields and routes: open the OpenAPI document.
curl -s https://api-dev.voxfi.app/api/agent/openapi.json -o voxfi-agent.json
openapi-generator generate -i voxfi-agent.json -g python -o ./clientCreating the bot, generating a new key and retiring it all happen in the app, so none of that is in the document.