---
name: furstock-openapi
description: Build trading bots, scripts, or custom clients for Furstock (furstock.trade), a Korean character paper-trading exchange, using its public REST OpenAPI and realtime WebSocket. Use when the user mentions furstock / 퍼스톡, wants to automate trading, market data, portfolio, or community actions there, or gives a furstock.trade/developers or api.furstock.trade URL.
---

# Furstock OpenAPI

Furstock (퍼스톡, https://furstock.trade) lists fictional characters as "stocks" and lets users trade them with virtual KRW ("원", no real value). The OpenAPI lets a user's own program do everything the web client does on **their own account**: market data, market/limit orders, holdings, order history, pending-order queue, community posts, watchlists, and more.

## Canonical sources (fetch these, don't guess)

| What                                    | URL                                                                                                                                                   |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Machine-readable spec (source of truth) | https://api.furstock.trade/openapi.json                                                                                                               |
| Human guide (Korean)                    | https://furstock.trade/developers                                                                                                                     |
| All guide pages in one markdown file    | https://furstock.trade/llms-full.txt                                                                                                                  |
| Per-topic markdown                      | https://furstock.trade/skills/furstock-openapi/references/{overview,authentication,conventions,market,account,orders,community,realtime,bot-guide}.md |

When a detail matters (field names, enums, limits), read `openapi.json` or the relevant reference file instead of relying on this summary.

## Workflow for building a bot

1. **Credentials.** The user issues an API key at https://furstock.trade/profile?tab=api and gets `client_id` (`fsk_…`) + `client_secret` (`fss_…`, shown once). Read them from env vars (`FURSTOCK_CLIENT_ID`, `FURSTOCK_CLIENT_SECRET`). Never hard-code or commit them, and never ask the user to paste the secret into chat if an env var or secrets file works.
2. **Token.** `POST https://api.furstock.trade/oauth2/token`, form-urlencoded `grant_type=client_credentials&client_id=…&client_secret=…` → `{access_token, token_type:"Bearer", expires_in:3600}`. There is no refresh token: cache the token and re-request it about 5 minutes before expiry. Don't request a token per call (30/min limit per client_id).
3. **Calls.** Send `Authorization: Bearer <access_token>` to `https://api.furstock.trade`. JSON in, JSON out, camelCase fields.
4. **Resilience** (required in every bot you write):
   - `429` → sleep `Retry-After` seconds, then retry. Limit: 10 req/s per key, burst 20.
   - `401` → drop the cached token, fetch a new one, retry once. If it persists, the key was revoked.
   - Orders: generate one UUID `clientToken` per logical order and **reuse it on retries**. The server dedupes, so a retried order never double-fills.
5. **Check order outcome in the body, not the HTTP status.** A rejected order is HTTP 200 with `status: "rejected"` and a `rejectReason`.

## Endpoint map

Public (no token needed): stocks, market data, community reads.

| Need                                           | Call                                                                                                         |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Stock list (≤1000, includes hidden)            | `GET /v1/stocks?limit=`                                                                                      |
| Search stocks (name/tag, excludes hidden, ≤50) | `GET /v1/stocks?q=`                                                                                          |
| Several stocks (≤100, includes hidden)         | `GET /v1/stocks?symbol=A&symbol=B` (repeat `symbol`; prefer over the full list when you need a few)          |
| One stock                                      | `GET /v1/stocks/{symbol}`                                                                                    |
| Price snapshot + synthetic orderbook           | `GET /v1/market/price/{symbol}`                                                                              |
| Price snapshots for many symbols (≤100)        | `GET /v1/market/prices?symbol=A&symbol=B` (repeat `symbol`; prefer over per-symbol calls)                    |
| Candles (≤500, gap-filled, KST buckets)        | `GET /v1/market/candles/{symbol}?unit=Nm\|1d\|1w\|1M\|1y&before=` (older page: `before`=oldest `time`)       |
| Recent trades                                  | `GET /v1/market/trades/{symbol}?limit=`                                                                      |
| Popular stocks (last session by trading value) | `GET /v1/market/popular?limit=` (updated once per 3h session close)                                          |
| Home rankings (popular/volume/gainers/losers)  | `GET /v1/market/rankings?type=value\|volume\|gainers\|losers&limit=`                                         |
| Cash / buying power                            | `GET /v1/accounts/buying-power` → `{cashBalance}`                                                            |
| Holdings                                       | `GET /v1/accounts/holdings`                                                                                  |
| Place order                                    | `POST /v1/orders` `{symbol, side:"buy"\|"sell", type:"market"\|"limit", quantity, limitPrice?, clientToken}` |
| Order history                                  | `GET /v1/orders?status=&symbol=&limit=(≤200)&before=<createdAt cursor>`                                      |
| Pending limit-order queue                      | `GET /v1/orders?status=pending`                                                                              |
| Cancel pending limit order                     | `POST /v1/orders/{orderId}/cancel` (409 `not_pending` if already filled or cancelled)                        |
| Community read / write / edit                  | `GET`/`POST /v1/community/{symbol}` `{body}`, `PATCH /v1/community/posts/{postId}`                           |
| Watchlist                                      | `GET /v1/watchlist`, `POST`/`DELETE /v1/watchlist/{symbol}`                                                  |
| Who am I                                       | `GET /v1/me` → `{userId, isAdmin, authMethod}`                                                               |

Other areas (item groups, stock journals/news, gallery, character listing applications, feedback) are in `openapi.json` and `references/community.md`.

**Not available with an API key** (403 `session_required`): issuing, listing, or revoking API keys; account withdrawal. Admin privileges never apply to API-key calls.

## Market rules that change bot logic

- **Symbols are character names, possibly Korean** (e.g. `보팔래빗`), 1–20 chars of unicode letters/digits plus `-` `.` `_` `~` `(` `)` (no spaces). URL-encode them in paths.
- **24/7 market.** Sessions roll over every 3 hours KST (00,03,…,21h). The new session's open = previous last price.
- **Price band** ±30% of session open (floor 500원). Market orders fill up to the band, then `partial`.
- **Circuit breaker**: ±15% from session open halts that stock for 5 minutes (`circuit_breaker`).
- **Pricing is a bonding curve, not an order book.** Buying pushes the price up along the curve, so large orders get worse average prices. The orderbook endpoint is synthetic, derived from the curve.
- **Limit orders** wait as `pending` and fill **all-or-nothing** when both the last price and the full-quantity average fill price satisfy the limit. No cash or shares are reserved while pending. Unfilled limits are auto-cancelled at session rollover (`session_settlement`).
- **Costs**: fee 0.15% on every fill, plus 0.2% tax on sells.
- **Starting cash** is 300,000원. No deposits.
- `tradingHalted: true` → orders rejected (`admin_halted`). If an admin freezes your account, every order is rejected with `account_frozen` (pending limit orders are cancelled with the same reason) — stop trading, retrying won't help. `hidden: true` → excluded from search and rankings but still tradable (`hideLocked: true` = placed under supervision by an admin, `false` = hidden by the listing owner).

## Realtime

`wss://ws.furstock.trade/socket` is a Phoenix Channels endpoint (serializer v2), receive-only, no auth. Join topic `stock:{symbol}` with params `{last_seq}` and receive `snapshot` / `price` (same shape as `GET /v1/market/price/{symbol}`) or `cold`. Send a heartbeat `[null, ref, "phoenix", "heartbeat", {}]` every 30s. With JS, the `phoenix` npm package handles this. Details and a Python example: `references/realtime.md`. Browsers on non-furstock origins are currently refused; server-side programs connect fine.

## Guardrails

- It is paper trading, but the user's in-game balance and rank are real to them. Confirm with the user before running a bot that places live orders. Offer a dry-run or logging mode first.
- Community posts from bots are public and subject to https://furstock.trade/policy. Don't write loops that spam posts.
- Stay under the rate limit on purpose: poll every few seconds at most, or use the WebSocket for prices.
- Don't scrape the web app or call undocumented endpoints. Everything needed is in `openapi.json`.

## Minimal TypeScript skeleton

```ts
const API = "https://api.furstock.trade";
let token = "",
  exp = 0;
async function auth() {
  if (Date.now() < exp - 300_000) return token;
  const r = await fetch(`${API}/oauth2/token`, {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.FURSTOCK_CLIENT_ID!,
      client_secret: process.env.FURSTOCK_CLIENT_SECRET!,
    }),
  });
  const b = await r.json();
  token = b.access_token;
  exp = Date.now() + b.expires_in * 1000;
  return token;
}
async function api(path: string, init: RequestInit = {}) {
  for (let i = 0; i < 5; i++) {
    const r = await fetch(API + path, {
      ...init,
      headers: { "content-type": "application/json", authorization: `Bearer ${await auth()}` },
    });
    if (r.status === 429) {
      await new Promise((s) => setTimeout(s, 1000 * Number(r.headers.get("retry-after") ?? 1)));
      continue;
    }
    if (r.status === 401) {
      exp = 0;
      continue;
    }
    if (!r.ok) throw new Error(`${path} ${r.status} ${await r.text()}`);
    return r.json();
  }
}
// const order = await api("/v1/orders", { method: "POST", body: JSON.stringify({ symbol, side: "buy", type: "market", quantity: 1, clientToken: crypto.randomUUID() }) });
```

A full TypeScript and Python example bot is in `references/bot-guide.md`.
