Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: BASE167 Games API
version: 1.0.0
summary: Claim BASE167 and take part in Degen Games without a browser.
description: 'Every write action follows one shape: POST a wallet address, receive a short-lived server-signed voucher plus ready-to-send calldata in `call`, then submit one transaction from that wallet. No login, session, cookie or CAPTCHA. Vouchers bind msg.sender, so the sending wallet must be the wallet the voucher was issued for. Prizes have no claim function and are paid manually. There is also a remote MCP server at https://base167.com/mcp exposing these same actions as named tools, with no API key and no OAuth.'
servers:
- url: https://base167.com
tags:
- name: Games
paths:
/api/games/claim-entry:
post:
operationId: claimEntry
summary: Get a voucher to mint this season's Entry pass
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- address
properties:
address:
$ref: '#/components/schemas/Address'
responses:
'200':
description: Eligibility result, with a voucher and prepared call when eligible. Outside the Pass window (any phase other than seatMarket or setup) this returns eligible:false with the phase, not an error.
content:
application/json:
schema:
type: object
required:
- eligible
properties:
eligible:
type: boolean
reason:
type: string
season:
type: integer
signature:
$ref: '#/components/schemas/Hex'
expiry:
type: integer
call:
$ref: '#/components/schemas/PreparedCall'
alreadyClaimed:
type: boolean
description: True when this wallet already holds this season's Entry pass. No voucher is issued; a second claim would revert AlreadyClaimed.
phase:
type: string
enum:
- pre
- seatMarket
- setup
- preGame
- game
- settled
description: The season phase this answer was computed in. Passes are claimable only in seatMarket and setup.
opensAt:
type: integer
description: Unix seconds at which the Entry pass window opens. Present only in phase 'pre', where the window is still ahead.
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: The snapshot for this season has not been taken yet.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Games
/api/games/enroll:
post:
operationId: enroll
summary: Register a competition wallet against an Entry pass
description: 'Only during the setup phase. Any wallet may be enrolled, including a smart account, and prior history is fine. What is enforced is the balance at the opening bell: at least $5 USDC, at most 0.0002 ETH of gas, and nothing else of value. Scoring counts USDC only, so an unscored asset sold mid-season would read as profit.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- player
- gameWallet
properties:
player:
$ref: '#/components/schemas/Address'
gameWallet:
$ref: '#/components/schemas/Address'
responses:
'200':
description: Voucher and prepared call for SeasonFactory.enroll.
content:
application/json:
schema:
type: object
required:
- ok
- season
- tokenId
- signature
- expiry
- call
properties:
ok:
const: true
season:
type: integer
tokenId:
type: integer
signature:
$ref: '#/components/schemas/Hex'
expiry:
type: integer
call:
$ref: '#/components/schemas/PreparedCall'
'400':
$ref: '#/components/responses/BadRequest'
'403':
description: 'The player holds no Entry pass for this season. IMPORTANT: ownership is read at the chain''s `safe` block, which trails the head by roughly 15-35 blocks (about 30-60 seconds on Base). A pass minted moments ago is therefore not visible yet, and this 403 is expected and temporary -- wait for the safe tag to pass your mint and retry rather than treating it as final.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: Enrolment window closed, or the competition wallet belongs to another player.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: The competition wallet was rejected on a rule. Enrolment does NOT reject a wallet for having code or a history; the balance rule is enforced later, at the opening bell.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Games
/api/games/claim-winner:
post:
operationId: claimWinner
summary: Get a voucher to mint a rank-stamped Winner badge
description: 'Mints an NFT only. It moves no value: prize payouts are manual and have no claim function.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- address
properties:
address:
$ref: '#/components/schemas/Address'
responses:
'200':
description: Eligibility result, with rank, voucher and prepared call when eligible.
content:
application/json:
schema:
type: object
required:
- eligible
properties:
eligible:
type: boolean
season:
type: integer
rank:
type: integer
signature:
$ref: '#/components/schemas/Hex'
expiry:
type: integer
call:
$ref: '#/components/schemas/PreparedCall'
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: Final results are not published yet.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Games
/api/games/set-name:
post:
operationId: setName
summary: Set or clear the display name shown against a wallet
description: 'Off-chain: this stores a label on our server and sends no transaction, so there is no prepared call. The caller signs a plain text message with the wallet being named. Smart-contract wallets are supported (ERC-1271, ERC-6492 for counterfactual accounts, ERC-8010 for 7702-delegated EOAs), so a Base Account works. Send an empty name to clear it.
The message must byte-match exactly:
Set my Degen Games name to: {name}
Domain: base167.com Degen Games
Expires: {expiry}
{name} is the trimmed name, {expiry} is unix seconds. Expiry must be in the future and no more than 900 seconds ahead. Sign it as a personal_sign / EIP-191 message, the same string, with no extra whitespace or trailing newline.
A name ending in .eth must be one the wallet actually resolves to, or the request is a 403.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- address
- name
- signature
- expiry
properties:
address:
$ref: '#/components/schemas/Address'
name:
type: string
description: Trimmed display name. Empty string clears it.
signature:
$ref: '#/components/schemas/Hex'
expiry:
$ref: '#/components/schemas/SignatureExpiry'
responses:
'200':
description: The name was stored.
content:
application/json:
schema:
type: object
required:
- ok
- name
properties:
ok:
const: true
name:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/BadSignature'
'403':
description: The name is a Basename or ENS name this wallet does not own.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: The name failed the content rules (length, characters, or a blocked word).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/VerifyUnavailable'
tags:
- Games
/api/games/set-handle:
post:
operationId: setHandle
summary: Set or clear the X handle shown against a wallet
description: 'Identical in shape to set-name, and equally off-chain: no transaction, no prepared call. The signed message uses a DIFFERENT prefix on purpose, so a signature captured for one action can never be replayed as the other.
The message must byte-match exactly:
Set my Degen Games X handle to: {handle}
Domain: base167.com Degen Games
Expires: {expiry}
{handle} is the trimmed handle, {expiry} is unix seconds, valid for at most 900 seconds. Send an empty handle to clear it.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- address
- handle
- signature
- expiry
properties:
address:
$ref: '#/components/schemas/Address'
handle:
type: string
description: Trimmed X handle. Empty string clears it.
signature:
$ref: '#/components/schemas/Hex'
expiry:
$ref: '#/components/schemas/SignatureExpiry'
responses:
'200':
description: The handle was stored.
content:
application/json:
schema:
type: object
required:
- ok
- handle
properties:
ok:
const: true
handle:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/BadSignature'
'422':
description: The handle failed the content rules.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
'503':
$ref: '#/components/responses/VerifyUnavailable'
tags:
- Games
/api/games/leaderboard:
get:
operationId: getLeaderboard
summary: The live standings, top holders and the collector board
description: 'The live or final standings, plus the top holders and the live collector board. Two different freshness facts apply and should not be conflated: the response is cached for 60 seconds, while the holder snapshot behind `updatedAt` is refreshed daily by a scheduled job and can be up to a day old. Available whether or not a season is running, which is why the standalone leaderboard page is always live.'
responses:
'200':
description: Standings, holders and collectors, each enriched with a resolved identity.
content:
application/json:
schema:
type: object
required:
- updatedAt
- holders
- collectors
- standings
properties:
updatedAt:
type: integer
description: Unix seconds the holder snapshot was taken. Refreshed daily by a scheduled job, so this can be up to a day old -- it is not the 60-second response cache. 0 means no snapshot exists yet.
holders:
type: array
items:
$ref: '#/components/schemas/BoardRow'
collectors:
type: array
items:
$ref: '#/components/schemas/BoardRow'
standings:
type:
- object
- 'null'
description: Live or final scores -- the same board /api/games/state returns, ranked by percentage gain. Null until the opening bell has passed and the first scoring run has completed, which is normal and not an error. A final board appears here only once it has been published.
tags:
- Games
/api/games/results:
get:
operationId: getResults
summary: Published final results for every completed season
description: A season appears here only after its final results are published. An empty list is the normal answer before the first season ends, not an error. Winner entries carry a rank and the badge that can be minted for it -- they do not carry a prize amount, because prizes are paid manually and have no claim function.
responses:
'200':
description: Every published season, newest first.
content:
application/json:
schema:
type: object
required:
- seasons
properties:
seasons:
type: array
items:
type: object
tags:
- Games
/api/games/state:
get:
operationId: getState
summary: Season, phase, config and boards
description: 'The live source for anything that changes: never cache these values in a static file.'
responses:
'200':
description: Current season state.
content:
application/json:
schema:
type: object
properties:
season:
type: integer
phase:
type: object
properties:
phase:
type: string
enum:
- pre
- seatMarket
- setup
- preGame
- game
- settled
description: 'pre: before the snapshot. seatMarket: snapshot taken, Entry passes claimable and tradeable. setup: the enrolment and funding window. preGame: enrolment closed, waiting for the opening bell. game: trading, between the bells. settled: the closing bell has passed.'
nextBoundary:
type: integer
description: Unix seconds at which the phase changes next.
description: Where the season is right now. Read this rather than comparing the timestamps yourself. The six values are the ones the server actually emits -- until 2026-08-29 this schema published pre/setup/live/final, of which only two existed, so an agent branching on 'live' or 'final' never matched.
config:
$ref: '#/components/schemas/SeasonConfig'
holders:
type: array
items:
$ref: '#/components/schemas/BoardRow'
description: The holder snapshot for this season, ranked. Present once the snapshot has been taken; trimmed to displayHolders rows, which is a DISPLAY bound and not the eligibility bound (topHolders).
enrolled:
type:
- object
- 'null'
description: Who has registered a competition wallet. Null before enrolment opens.
properties:
total:
type: integer
description: Everyone enrolled, not just the rows returned.
rows:
type: array
items:
$ref: '#/components/schemas/BoardRow'
description: 'Trimmed to displayRoster: every row costs an eth_call on a cache miss.'
collectors:
type: array
items:
$ref: '#/components/schemas/BoardRow'
description: 'The collector board: current Entry pass holders, ranked by pass count then BASE167 balance. finalsCollectorSeats Finals seats are drawn from it.'
leaderboard:
type:
- object
- 'null'
description: Live or final scores. Null until the opening bell has passed and the first scoring run has completed -- which is normal, not an error.
tags:
- Games
components:
schemas:
SignatureExpiry:
type: integer
description: Unix seconds. Must be in the future and at most 900 seconds ahead; the same value must appear in the signed message. It bounds the replay window, which is why an unbounded expiry is rejected rather than ignored.
Error:
type: object
required:
- error
properties:
error:
type: string
Address:
type: string
pattern: ^0x[a-fA-F0-9]{40}$
description: A mixed-case address MUST carry a valid EIP-55 checksum; an all-lowercase address is always accepted. A correctly-shaped address with a bad checksum returns 400 BAD_CHECKSUM.
Hex:
type: string
pattern: ^0x[a-fA-F0-9]*$
BoardRow:
type: object
description: One ranked wallet. `identity` carries the resolved display name, Basename or X handle where one is known.
required:
- address
properties:
address:
$ref: '#/components/schemas/Address'
identity:
type: object
PreparedCall:
type: object
description: A ready-to-send transaction. Submit it from the same wallet the voucher was issued for; the contract binds msg.sender.
required:
- chainId
- to
- data
- value
properties:
chainId:
type: integer
const: 8453
to:
$ref: '#/components/schemas/Address'
data:
allOf:
- $ref: '#/components/schemas/Hex'
description: 'Calldata for the action, with an ERC-8021 builder-code suffix appended after the ABI arguments. The suffix is data the contracts ignore -- it attributes the transaction to base167 on Base''s builder dashboard -- and every action built here carries it, whether it came from the browser, this API or the MCP server. Send the bytes EXACTLY as given: re-encoding the call from its arguments drops the suffix, and trimming it changes nothing else about what the transaction does.'
value:
type: string
const: '0x0'
SeasonConfig:
type: object
description: The season's settings and addresses. Everything that decides WHO may enter and WHEN. Until 2026-08-29 this was published as a bare {"type":"object"}, so the single richest object in the API was undocumented. Timestamps are unix seconds; amounts are raw integer strings in the token's own decimals, never floats.
properties:
season:
type: integer
description: Season number. Seasons are sequential and only one is current.
chainId:
type: integer
const: 8453
description: Base mainnet.
base167:
$ref: '#/components/schemas/Address'
description: The BASE167 token.
factory:
$ref: '#/components/schemas/Address'
description: 'SeasonFactory: claimEntry, enroll and claimWinner all target this.'
entryNFT:
$ref: '#/components/schemas/Address'
description: This season's Entry pass (ERC-721).
winnersNFT:
$ref: '#/components/schemas/Address'
description: This season's Winner badge (ERC-721). Minting one moves no value.
usdc:
$ref: '#/components/schemas/Address'
description: The USDC contract scoring is measured in.
entryMarketplaceUrl:
type: string
description: Where Entry passes can be bought or sold during seatMarket.
exclusions:
type: array
items:
$ref: '#/components/schemas/Address'
description: 'Addresses removed from the holder snapshot by design: the treasury, the burn address, the token itself and the liquidity pool.'
snapshotAt:
type: integer
description: Unix seconds. The holder snapshot is taken; the Entry pass becomes claimable.
enrollOpenAt:
type: integer
description: Unix seconds. Enrolment opens.
enrollCloseAt:
type: integer
description: Unix seconds. Enrolment closes.
openingBellAt:
type: integer
description: Unix seconds. Trading starts and the competition wallet is judged on its balances.
closingBellAt:
type: integer
description: Unix seconds. Trading ends; scoring and publication follow.
snapshotBlock:
type:
- integer
- 'null'
description: Block the snapshot was pinned to. Null until the job runs; written once and never changed.
startBlock:
type:
- integer
- 'null'
description: Block the opening bell was pinned to. Null until the bell; one-shot.
closingBlock:
type:
- integer
- 'null'
description: Block the closing bell was pinned to. Null until the bell; one-shot.
topHolders:
type: integer
description: 'ELIGIBILITY: how many wallets may claim an Entry pass. Read this rather than assuming a number -- it has changed between seasons.'
displayHolders:
type: integer
description: 'DISPLAY ONLY: how many rows the site renders. Not an eligibility bound. Do not confuse it with topHolders.'
displayRoster:
type: integer
description: 'DISPLAY ONLY: how many enrolled rows the site resolves and renders.'
winnersCount:
type: integer
description: 'How many places count as winners: each mints a rank-stamped Winner badge and takes a Finals seat. NOT how many are PAID -- only the podium is paid.'
minStakeUSDC:
type: string
description: 'Raw USDC (6dp) a competition wallet must hold at the opening bell. "5000000" is $5. There is no ceiling: ranking is by percentage return, so a larger stake buys no advantage.'
stakeToleranceUSDC:
type: string
description: Raw USDC (6dp) of slack allowed under minStakeUSDC. "500000" is $0.50.
gasAllowanceWei:
type: string
description: Maximum native ETH, in wei, the competition wallet may hold at the opening bell. "200000000000000" is 0.0002 ETH. More than this is a disqualification.
finalsCollectorSeats:
type: integer
description: How many Finals seats are drawn from the collector board.
base167DeployBlock:
type: integer
description: Block the BASE167 token was deployed at. The floor for any log scan -- start here rather than at block 0, or an eth_getLogs range will be rejected.
entryNFTDeployBlock:
type:
- integer
- 'null'
description: 'Block this season''s EntryNFT was deployed at, and the floor for any scan of ITS logs. Optional: a season synced before 2026-09-02 has no pin and falls back to base167DeployBlock, which is correct but scans a far wider range.'
prizePotBase167:
type: integer
description: The season prize pot in WHOLE BASE167 (not wei). Paid to the podium only. Always stated in BASE167 and never as a cash value.
prizeBreakdown:
type: array
description: The podium split. Sums to prizePotBase167 and descends strictly. Places outside it mint a Winner badge and take a Finals seat but are paid nothing.
items:
type: object
required:
- place
- amount
properties:
place:
type: integer
amount:
type: integer
description: Whole BASE167.
required:
- season
- chainId
- factory
- entryNFT
- winnersNFT
- snapshotAt
- enrollOpenAt
- enrollCloseAt
- openingBellAt
- closingBellAt
- topHolders
- winnersCount
- minStakeUSDC
- gasAllowanceWei
- prizePotBase167
- prizeBreakdown
responses:
BadRequest:
description: Malformed JSON, address or signature.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
properties:
code:
type: string
enum:
- INVALID_ADDRESS
- BAD_CHECKSUM
description: BAD_CHECKSUM means the address is correctly shaped but its mixed-case EIP-55 checksum does not verify -- send it all-lowercase or fix the checksum. INVALID_ADDRESS means it is not an address at all.
reason:
type: string
BadSignature:
description: The signature did not verify against this address. The usual cause is a message that does not byte-match the template, most often a different expiry, a trimmed name, or an added newline.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
VerifyUnavailable:
description: 'No chain could be reached to verify the signature, so it is unknown whether it is good. This is NOT a rejection: retry. Reporting it as a bad signature would send a smart-wallet holder off to debug a wallet that works.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: 'Over 5 requests per minute. Retry-After: 60.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'