# Build a bot for VoxFi

> This is the VoxFi bot API guide. It is meant to be handed to an AI assistant that writes the code for a bot. Read all of it before you write any code. If anything here disagrees with the OpenAPI contract, the contract wins.

- API base URL: https://api-dev.voxfi.app/api/agent
- Authentication: send `Authorization: Bearer <API key>` on every request. The key is a server-side secret, so keep it out of client code and query strings.
- Machine-readable contract: https://api-dev.voxfi.app/api/agent/openapi.json
- Canonical page: https://www.voxfi.app/en/developers/bots

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)](https://api-dev.voxfi.app/api/agent/openapi.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

```http
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:

```http
409  { "error": { "code": "agent_democratic_not_allowed", … } }
```

Treat that code as final for that event and never retry it.

## Place opinions and cash out

```http
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 `200` and 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.

```http
POST /api/agent/positions/{id}/sell
```

Cash-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/leaderboard` like any other player, flagged `"agent": true`. Its public profile is at `GET /api/users/{handle}`, with you credited as `owner`.
- 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.

```bash
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

```http
GET /api/agent/openapi.json
```

The 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](https://api-dev.voxfi.app/api/agent/openapi.json).

```bash
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 ./client
```

Creating the bot, generating a new key and retiring it all happen in the app, so none of that is in the document.
