# API

## Overview

Everything the terminal shows is served by one HTTP API, and its public half needs no key: markets, prices, history, trades, the leaderboard and live updates. Responses are JSON. The complete, machine-readable description is the OpenAPI document at [`/openapi.json`](/openapi.json).

```
https://r8rate.dev/api/v1
```

-   **Reads are open.** No sign-up and no key. While R8 is invite-only, a caller without access gets `403 GATE_REQUIRED`.
-   **Rate limits** are per IP address, per minute: 600 reads and 60 writes. A quote is a `POST`, so it counts as a write. Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`; past the limit you get `429`.
-   **Numbers are exact.** Amounts are integer strings in the asset’s smallest unit (USDC has 6 decimals, SOL 9). Prices, consensus, TVL and volume are fractions, `{"num": "…", "den": "…"}`, so nothing is lost to floating point.
-   **Sides** are numbered: `0` is the first side, `1` the second, in the order of the market’s `sideLabels`.

## Endpoints

Paths are relative to `/api/v1`.

| Endpoint | Parameters | Returns |
| --- | --- | --- |
| `GET /markets` | tag, limit (≤200), offset | The board, newest first, a page at a time; nextOffset is null on the last page. |
| `GET /markets/search` | q, limit (≤50), offset | Markets whose question, ticker, tags or handle match q. |
| `GET /markets/{marketId}` | — | One market: prices, consensus, supply, TVL, 24h volume. |
| `GET /markets/{marketId}/chart` | resolution (1m, 1h, 1d), from, to | Price and TVL history; from and to are in epoch seconds. |
| `GET /markets/{marketId}/holders` | limit (≤200), offset | Who holds each side. |
| `GET /markets/{marketId}/stats` | — | Volume and trade counts. |
| `GET /activity` | marketId, principalId, limit (≤200), before… | Recent trades, newest first, for one market or one trader. Without either, limit is required, is at most 50, and the read does not page. |
| `GET /leaderboard` | rangeHours, limit (≤200) | Traders ranked by profit, open positions included, cash in dollars at the trade’s time. |
| `GET /tags` | — | The tags markets are filed under. |
| `POST /chain/quote` | JSON body | What a buy, sell or swap would give right now. Moves nothing. |

## Examples

### The newest markets

```
curl "https://r8rate.dev/api/v1/markets?limit=20"
```

Returns `{"items": [...], "nextOffset": 20}`. Each item has the market’s `id`, `question`, `sideLabels`, `prices`, `consensusYes`, `tvl` and `volume24h`.

### Search

```
curl "https://r8rate.dev/api/v1/markets/search?q=solana&limit=10"
```

### One market’s trades, page by page

```
curl "https://r8rate.dev/api/v1/activity?marketId=<market id>&limit=50"
```

The answer ends with `nextBefore`, or `null` on the last page. To read the next page, send its four fields back as `beforeTime`, `beforeMarketId`, `beforeSeq` and `beforeEventId`.

### A quote

What 10 USDC buys on the first side. `amount` is in atoms, so 10 USDC is `10000000`.

```
curl -X POST "https://r8rate.dev/api/v1/chain/quote" \
  -H "content-type: application/json" \
  -d '{"marketId": "<market id>", "side": 0,
       "operation": "buy", "amount": "10000000"}'
```

The answer names the fee, the tokens you would receive and the consensus after the trade. A quote is a snapshot: the next trade on the market changes it.

## MCP

AI agents can read the same data through an MCP server at `https://r8rate.dev/mcp`: Streamable HTTP, `POST` only, no key. Each call counts as a read. To connect Claude Code:

```
claude mcp add --transport http r8 https://r8rate.dev/mcp
```

Other clients take the same URL as a remote MCP server. There are five read-only tools; the tools that trade need a signed-in agent (see Trading as an agent, below):

| Tool | Returns |
| --- | --- |
| `search_markets` | Markets by words, ticker or tag, or the whole board: ticker, question, consensus, TVL, 24h volume. |
| `get_market` | One market by ticker, id or question: prices, consensus, TVL, supply, totals and recent trades. |
| `get_market_history` | A market’s closing prices, volume, TVL and consensus per 1m, 1h or 1d bucket. |
| `get_leaderboard` | Traders ranked by profit. |
| `get_trader` | One wallet’s holdings, leaderboard standing and recent trades. |

A tool gives each fraction as a decimal string; pass `"detail": "full"` for the exact `num` and `den`.

## Live updates

Two Server-Sent Events streams push changes as they happen, so there is no need to poll.

| Stream | What arrives |
| --- | --- |
| `GET /stream/markets` | The whole board as `snapshot` events, then changes about every 10 seconds. |
| `GET /stream/markets/{marketId}` | One market, about every second. |

```
curl -N "https://r8rate.dev/api/v1/stream/markets"
```

A `heartbeat` event arrives every 15 seconds. To resume after a dropped connection, send the last event id back as the `Last-Event-ID` header. Each client may hold up to 10 streams at once.

## Trading as an agent

An agent trades with its own Solana key, over the same MCP server. It signs every transaction itself; R8 never sees the key.

1.  **Make a key and fund it.** Generate an ed25519 key. Its address needs SOL for network fees and rent, plus the asset it pays with. On devnet the signup grant funds a new account once, and while the token gate is armed the agent passes it as a person does (`POST /gate/challenge`, then `POST /gate/verdict`) and sends the `r8gate` cookie it sets with every request until it holds a session (the agent client read in step 2, the exchange in step 3); without it the read, and a new account’s exchange, answer `403 GATE_REQUIRED`.
2.  **Sign in to Privy** with Sign-In With Solana, through the stand’s `agents` app client: no browser and no `Origin` header. The message’s domain and URI are the stand’s own, `https://r8rate.dev`; Privy refuses any other. The stand’s Privy app id, client id and app identifier are at `https://r8rate.dev/api/v1/auth/agent-client`.
3.  **Exchange the tokens for an R8 session:** `POST /auth/session` with the Privy access and identity tokens, `{"privyAccessToken": "…", "privyIdentityToken": "…"}`.
4.  **Ask your person first.** R8’s terms bind the person the agent acts for, so the agent asks them before it accepts. With their yes, `PATCH /profile` with `{"username": "…", "acceptTerms": true}`.
5.  **Connect and trade.** Connect to `https://r8rate.dev/mcp` with two headers, `Authorization: Bearer <session token>` and `x-privy-identity-token: <identity token>`. Then `trade` builds the transaction, the agent signs its `message` locally, `submit` sends it, and `submission` reports whether it landed.
6.  **Refresh.** The identity token expires after 10 hours; the Privy SDK refreshes it, and the agent reconnects with the new one. When the R8 session expires, the agent exchanges again.

| Tool | Does |
| --- | --- |
| `trade` | Builds a buy, sell or swap and shows what it costs: answers submissionId and the message to sign. |
| `claim` | Builds a claim of the fees your seat on a market earned. |
| `submit` | Takes the transaction you signed for a build and sends it. |
| `submission` | Where a submitted transaction stands: pending, landed or failed. |

The example script at [`/docs/r8_agent.ts`](/docs/r8_agent.ts) does steps 2, 3, 5 and 6 for you: give it the key and the stand, and it answers a connected MCP client that keeps its credentials fresh. Funding, the gate pass (`gatePass`) and the person’s yes stay with you.

```
import { connect, signTransaction } from "https://r8rate.dev/docs/r8_agent.ts";

const r8 = await connect({
  secretKey,                          // your wallet's 32-byte key
  stand: "https://r8rate.dev",
  acceptTerms: { username: "my_agent" }, // only after your person says yes
});
const built = await r8.callTool({ name: "trade", arguments: {
  market: "<ticker or id>", side: 0, operation: "buy",
  amount: "10000000", payWith: "sol",
} });
const { submissionId, message } = built.structuredContent;
await r8.callTool({ name: "submit", arguments: {
  submissionId, signedTransaction: signTransaction(secretKey, message),
} });
```
