# SideBet.vip API

Peer-to-peer 1v1 bets on real-world events. Base URL: https://sidebet.vip

## Environments: FUN and REAL

SideBet.vip runs two completely separate environments:

- **FUN** — demo credits. Every new account starts with 1000 credits. Default everywhere.
- **REAL** — real-money balance. Starts at zero, funded by deposit, and requires a one-off
  acceptance of the real-money terms in the web app before any bet can be created or joined.

Balances, offers, bets, history, rankings, referral commissions and withdrawals never cross
between the two. An offer created in FUN can only be joined by a FUN bet, and the same for REAL.

How the environment is chosen for an API call:

- **API keys** are bound to one environment at creation time (Account → API keys) and always act
  in that environment. The active environment is returned as `mode` on `GET /me`.
- **OAuth tokens** default to `FUN`. Send the header `X-SideBet-Mode: REAL` to act in the
  real-money environment; the user must already have accepted the real-money terms.

Every bet, transaction and account response includes a `mode` field with the value `FUN` or
`REAL`. Listing endpoints only return objects from the caller's environment.

## Authentication

Generate a personal API key in your account page (Account → API keys). The secret is shown once.
Send it as a bearer token on every request:

    curl https://sidebet.vip/api/public/v1/me \
      -H "Authorization: Bearer sbv_live_..."

Keys are either **read** (GET only) or **full** (create, join, cancel, confirm).
Rate limit: 60 requests per minute per key.

### OAuth 2.0 (for third-party apps)

Other applications can act on behalf of a SideBet.vip user without ever seeing their password or API key.
The flow is standard OAuth 2.0 with PKCE and dynamic client registration.

- Protected resource metadata: `GET https://sidebet.vip/.well-known/oauth-protected-resource`
- Authorization server metadata: `GET https://xxfiotdooqtoeeegdach.supabase.co/auth/v1/.well-known/oauth-authorization-server`
- Consent page: `https://sidebet.vip/.lovable/oauth/consent`
- Recommended scopes: `openid email profile`

Register the client (or use dynamic registration), send the user to the authorization endpoint, exchange
the code for an access token, then call the API with that token:

    curl https://sidebet.vip/api/public/v1/me \
      -H "Authorization: Bearer <oauth access token>"

OAuth tokens have full scope (same as a `full` API key) and are not subject to the per-key rate limit.

## Endpoints

### Read scope

- `GET /api/public/v1/me` — your profile and wallet balance (available and locked credits).
- `GET /api/public/v1/categories` — bet categories with commission and confirmation window.
- `GET /api/public/v1/fixtures?league=Ekstraklasa` — upcoming matches for automatically settled bets.
- `GET /api/public/v1/weather-markets?city=Warsaw` — open weather markets (METAR station, local day, temperature line).
- `GET /api/public/v1/bets?status=OPEN&mine=false&limit=50` — open marketplace bets plus every bet you take part in.
- `GET /api/public/v1/bets/{betId}` — a single bet with positions and confirmations.
- `GET /api/public/v1/transactions?limit=100` — your credit history: stakes, wins, losses, refunds, commissions.

### Full scope

- `POST /api/public/v1/bets` — create a manual bet; your stake is locked immediately.
- `POST /api/public/v1/bets/match` — create a sports bet settled automatically from the official full-time result.
- `POST /api/public/v1/bets/weather` — create a weather bet settled automatically from the station's daily maximum.
- `POST /api/public/v1/bets/{betId}/join` — join an open bet by matching its stake.
- `POST /api/public/v1/bets/{betId}/cancel` — cancel your own unmatched bet; stake is refunded.
- `POST /api/public/v1/bets/{betId}/confirm` — confirm outcome of a manual bet: { "outcome": "WON" | "LOST" }.

## Creating bets

Manual bet:

    curl -X POST https://sidebet.vip/api/public/v1/bets \
      -H "Authorization: Bearer sbv_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "title": "BTC above 100k on Friday",
        "description": "Bitcoin closes above 100,000 USD on Friday 23:59 UTC.",
        "categoryId": "<category uuid>",
        "creatorSide": "YES",
        "stakeAmount": 100,
        "startsAt": "2026-09-10T10:00:00Z",
        "endsAt": "2026-09-12T23:59:00Z",
        "resolutionSource": "Binance BTC/USDT daily close"
      }'

Sports bet (auto-settled from official result; a draw voids the bet and both stakes are returned):

    curl -X POST https://sidebet.vip/api/public/v1/bets/match \
      -H "Authorization: Bearer sbv_live_..." \
      -H "Content-Type: application/json" \
      -d '{ "fixtureId": "2052881", "pick": "HOME", "stakeAmount": 50, "safeClose": true, "partialFill": true }'

Weather bet (auto-settled from the METAR station daily maximum):

    curl -X POST https://sidebet.vip/api/public/v1/bets/weather \
      -H "Authorization: Bearer sbv_live_..." \
      -H "Content-Type: application/json" \
      -d '{ "marketId": "wx:warsaw:2026-09-10", "pick": "ABOVE", "stakeAmount": 50, "safeClose": true, "partialFill": true }'

## Response shapes

All responses are JSON with camelCase keys. Collections are wrapped in a named array.

`GET /api/public/v1/weather-markets` →

    {
      "markets": [
        {
          "id": "wx:warsaw:2026-09-10",      // pass this as marketId
          "city": "Warsaw",
          "country": "Poland",
          "timezone": "Europe/Warsaw",
          "marketDate": "2026-09-10",         // local calendar day covered
          "thresholdC": 19.5,                 // the line, always in Celsius
          "forecastTmax": 21.3,               // Open-Meteo forecast, never settles
          "closesAt": "2026-09-09T22:00:00.000Z",   // local midnight, betting stops
          "dayEndAt": "2026-09-10T22:00:00.000Z",   // next local midnight
          "resolvesAt": "2026-09-10T22:10:00.000Z", // settlement, +10 minutes
          "stationIcao": "EPWA",
          "stationName": "Warsaw Chopin",
          "unit": "C",                        // "C" or "F" — the station's unit
          "bandLabel": "19°C",                // outcome band shown to users
          "lineLabel": "19–20°C",             // whole degrees straddling the line
          "aboveLabel": "20°C and above",     // wins for pick "ABOVE"
          "belowLabel": "19°C or lower"       // wins for pick "BELOW"
        }
      ]
    }

Market ids are always the string in `id` (format `wx:{city-slug}:{YYYY-MM-DD}`, e.g.
`wx:new-york:2026-09-10`). Do not build ids from the ICAO code — always pass back `id`.
`?city=` matches the `city` field case-insensitively as a substring.

`GET /api/public/v1/fixtures` →

    {
      "fixtures": [
        {
          "id": "2052881",                    // pass this as fixtureId
          "league": "Ekstraklasa",
          "homeTeam": "Legia Warszawa",
          "awayTeam": "Lech Poznań",
          "homeBadge": "https://.../legia.png",
          "awayBadge": null,
          "kickoffAt": "2026-09-12T17:30:00.000Z",
          "venue": "Stadion Wojska Polskiego"
        }
      ]
    }

`POST /api/public/v1/bets`, `/bets/match`, `/bets/weather` → `201` with

    { "betId": "08c5dfef-7d71-4a0a-9fad-001597e4bb2e" }

`POST /bets/{betId}/join|cancel|confirm` → `{ "ok": true }`.

### safeClose

Optional `safeClose` cancels an unmatched bet automatically and refunds the stake —
at kickoff for match bets, at 12:00 local city time for weather bets — so nobody can
join once the outcome is already leaning one way.

### partialFill

Optional `partialFill` (creator-only, auto-settled match/weather bets) lets an
unmatched bet pair with the biggest opposing offer on the same event at close time
instead of expiring. Only one pair is made: the bet with the higher stake absorbs the
smaller one at the smaller amount. The smaller bet closes with status `MERGED` (its
stake is refunded and its creator joins the bigger bet at the matched amount); any
surplus on the bigger side is refunded. The merged bet then settles normally from the
official result. If no opposing offer exists, normal expiry and refund apply. Bet
objects expose the choice as `partialFill`.

### Settlement rules

- Match bets always settle from the official full-time result. A draw voids the bet and
  both stakes are returned in full.
- Weather bets settle 10 minutes after local midnight from the METAR station's daily
  maximum temperature. The line always falls between two whole station degrees, so a tie
  is impossible. Non-US markets use Celsius (e.g. 19°C), US markets use a two-degree
  Fahrenheit band (e.g. 71–72°F).

## Webhooks

Set one https callback URL in your account. We POST JSON for `bet.matched`,
`bet.settled`, `bet.refunded` and `bet.disputed`. Every request carries
`x-sidebet-event` and `x-sidebet-signature`, an HMAC-SHA256 hex digest of the raw
body using your signing secret:

    const expected = crypto
      .createHmac("sha256", process.env.SIDEBET_WEBHOOK_SECRET)
      .update(rawBody)
      .digest("hex");
    if (expected !== req.headers["x-sidebet-signature"]) return res.status(401).end();

## Errors

Errors return a JSON body { "error": "..." } with status 400 (invalid request),
401 (bad key), 403 (read-only key), 404 (not found) or 429 (rate limit).
Withdrawals are not available through the API.

Human-readable docs: https://sidebet.vip/docs/api
This file: https://sidebet.vip/docs/api.md
