Lichess Games Export: Moves, Openings, Ratings, Tournaments avatar

Lichess Games Export: Moves, Openings, Ratings, Tournaments

Pricing

from $0.70 / 1,000 chess data rows

Go to Apify Store
Lichess Games Export: Moves, Openings, Ratings, Tournaments

Lichess Games Export: Moves, Openings, Ratings, Tournaments

Export a chess player's games from Lichess as rows: result, ratings, time control, opening, move list, clocks and accuracy, filtered by speed, rated flag, colour, opponent and date. Also player profiles with every variant rating, rating history for charts, arena standings and top-player boards.

Pricing

from $0.70 / 1,000 chess data rows

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Export a chess account's games from lichess.org as flat rows: opponent and both ratings, colour, result, time control, opening with its ECO code, the full move list, the clock after every move and the accuracy figures of the server analysis. The same actor also answers the three questions that sit next to a games export — how strong is this account (profile and every variant rating), how did its rating move day by day, and what was the final table of a tournament — plus the current top list of any speed or variant. No API key, no login, no proxy, no browser: the open Lichess API, read one request at a time. Made by Yadroo.

The games themselves are released by Lichess into the public domain (CC0). Rows carry public chess data only — account name, chess title, country code and FIDE rating where the player published them, ratings, counts, moves, standings. The free-text part of a profile (real name, biography, links) is deliberately never written to a row, and the source has no contact field to read.

Use cases

  • Feed an engine or a model — mode: "games" with includeMoves, includeClocks and includeAccuracy: every game arrives as one row with the moves in algebraic notation, the remaining time after each move and the server's accuracy, average centipawn loss and blunder counts. That is the step most review tools start with, without anyone downloading a file by hand.
  • Coaching and opening work — color: "black", perfTypes: ["rapid"], rated: "rated": one colour, one speed, newest first. openingName and openingEco make a repertoire table a group-by away.
  • Watch an account on a schedule — sinceHours: 24 with onlyNew: true: every run holds the games played since the last look and nothing else, so an empty run means the player did not play. No date editing between runs.
  • Export a whole career in instalments — sortDescending: false with onlyNew: true and a maxItems per run: each run continues forwards from the newest game the previous runs wrote.
  • A head-to-head record — opponent: "someUsername": only the games these accounts played against each other, in either colour. If they never met, the run says so instead of quietly returning everybody else's games.
  • A ratings table for a club page or a seeding list — mode: "player" with a list of usernames: rating and games per speed, win rate, title, country, FIDE rating and the join date, one row per account.
  • A rating curve for a chart or a report — mode: "ratingHistory": one row per day on which the rating moved, with the change against the previous point already computed. Nothing left to post-process. Read the note in Limits first: the source publishes this curve for part of the accounts only.
  • A tournament's final table — mode: "tournament" with the id from the URL: rank, account, title, score, rating and the tournament performance, in rank order, with the event name, clock and player count on every row.
  • Discover strong active accounts — mode: "leaderboard": the current top of any speed or variant, which doubles as a list of usernames to feed back into games mode.

Input

Each mode needs one thing and nothing else: usernames for games, player and ratingHistory, tournamentIds for tournament, and nothing at all for leaderboard. The examples in the table below sit in the schema as prefills, so a run started from the Apify Console already has usernames and tournamentIds filled in and works with one click. A call from the API, a client library or a task sends its own input: an empty body is answered with an error naming the field it needs, because a default account or tournament would silently turn every run into somebody else's query.

FieldTypeDefaultAllowed values / notes
modestringgamesgames, player, ratingHistory, tournament, leaderboard — see Modes
usernamesstring[]—Accounts as they appear after the @ in a profile URL. Prefilled ["DrNykterstein", "cutemouse83"]. Used by games, player, ratingHistory. A pasted https://lichess.org/@/name is accepted
perfTypesstring[]—Multi-select of speeds and variants. Empty = everything. Sent to the source in games mode
ratedstringanyany, rated, casual
colorstringanyany, white, black — the colour the named account played
opponentstring—A second username: keep only games against that one account, either colour
sinceHoursnumber—1–87600. Rolling window in UTC hours, counted from the moment the run starts and compared with the moment a game started. Takes precedence over dateFrom
dateFromstring—2026-01-01 or 2026-01-01T00:00:00Z. Fixed lower bound, compared with the moment a game started (measured: a game begun one minute before the bound is left out even if it ended after it)
dateTostring—Same formats. Fixed upper bound, exclusive, also by the start of the game
analysedOnlybooleanfalseKeep only games with a full computer analysis — the ones that carry accuracy and ACPL
onlyNewbooleanfalseWrite only games no earlier run of the same actor or task has written. See Scheduling
sortDescendingbooleantrueNewest games first. Off walks a career forwards (with onlyNew, each run continues where the last one stopped); also flips the rating-history order
includeMovesbooleantrueThe move list in algebraic notation plus movesCount
includeOpeningbooleantrueopeningEco, openingName, openingPly
includeClocksbooleanfalseThe remaining time of both sides after every move, in centiseconds. Makes a row several kB
includeEvalsbooleanfalseThe per-move engine evaluation of the server analysis, where one exists
includeAccuracybooleantrueAccuracy, average centipawn loss and the inaccuracy/mistake/blunder counts of both sides
sinceDaysnumber—1–7300. ratingHistory only: keep points of the last N days. Empty = the whole history
tournamentIdsstring[]—The eight characters from a tournament URL. Prefilled ["ayELljKv"]. A pasted full URL is accepted
tournamentKindstringarenaarena or swiss — see Arena and swiss
leaderboardPerfstringblitzOne of the 13 top-list speeds and variants
topCountnumber201–200. Length of the top list
maxItemsnumber501–2000. Hard cap on the rows written, and therefore on what the run costs. In games mode it is split over usernames. Your maximum cost per run is a second cap — see Limits
fieldsstring[]allKeep only these columns, in the order listed. The mode's key columns stay in every row even when not named: gameId + player (games), username + found (player), username + perfType (rating history), tournamentId + found (tournament), perfType + rank (top list); a found: false row also keeps found and notFoundReason. Letter case does not matter, and the words of a name may be joined or separated by spaces, _ or - (GameID, game_id, Opening Name); an unknown name is reported in the status message with the closest column, and a list with no valid name at all stops the run before any request

The fields with a fixed list of values (mode, perfTypes, rated, color, tournamentKind, leaderboardPerf) are checked by Apify against the schema before a run even starts, so a misspelled value (blizt, koth) is rejected there with the allowed values - pick them from the lists in Reference. What the actor itself cleans up is the free text: a pasted https://lichess.org/@/name or lichess.org/tournament/<id> is reduced to the id, casing and duplicate names do not matter, a column name in fields is matched without regard to case or to how its words are separated (opening_name and Opening Name mean openingName), and an unknown one is named in the status message. A search is never widened behind your back: an input that cannot be read fails with a message that says what to change.

Reference

Modes

One mode per run, one row shape per mode, one dataset view per mode. The other views stay empty.

ModeOne row isReads these inputsSource endpoint
gamesone finished game of one accountusernames, all game filters, all include* flagsgames export of an account
playerone account with its ratings and countsusernamesaccount profile
ratingHistoryone account, one speed, one dayusernames, perfTypes, sinceDays, sortDescendingrating history of an account — the source publishes it for part of the accounts only, see Limits
tournamentone player in a standingtournamentIds, tournamentKindtournament metadata + standing
leaderboardone place in a top listleaderboardPerf, topCounttop list of one speed or variant

Inputs that belong to another mode are ignored and the status message says so — they are never applied silently.

Speeds and variants

perfTypes accepts these keys. The first six are time classes of standard chess; the next eight are variants with their own rating; puzzle is not a game type at all and exists only as a rating curve.

KeyMeaning
ultraBullet30 seconds or less for the whole game
bulletunder 3 minutes
blitz3 to 8 minutes
rapid8 to 25 minutes
classicalover 25 minutes
correspondencedays per move
chess960randomised starting position
crazyhousecaptured pieces can be dropped
antichesscapturing is compulsory, losing everything wins
atomica capture explodes the neighbouring squares
hordepawns against a normal army
kingOfTheHillthe centre squares win the game
racingKingsfirst king to the eighth rank
threeCheckthree checks win the game
puzzlethe puzzle rating — ratingHistory mode only

In games mode several values mean OR and are sent to the source, so games outside the filter are never transferred and never charged. puzzle in games mode is dropped with a warning instead of being sent.

Top-list speeds and variants

leaderboardPerf takes the same keys except correspondence and puzzle, which have no public top list — 13 values, from ultraBullet to threeCheck. Each list is 200 accounts long at most.

Arena and swiss

The two tournament formats live under different URLs and an id of one is not an id of the other.

FormatURL to read the id fromtournamentKindRows carry
Arena — timed, pairs continuouslylichess.org/tournament/ayELljKv → ayELljKvarenascore, the tournament performance, perfType, and the streak sheet when the standing is read page by page
Swiss — fixed rounds, used by clubs and teamslichess.org/swiss/6iJdh9Ds → 6iJdh9Dsswisspoints and tieBreak, mapped into score as well; perfType stays empty because the swiss endpoint does not publish one

Finished tournaments stay readable, so an id keeps working long after the event. An id that does not exist in the chosen format yields one row with found: false and a reason that points at the other format. A tournament that exists but has not started yet yields one row with the event's metadata, an empty rank and username, and the reason in notFoundReason.

Result and status values

result is always from the named account's point of view: win, loss, draw, or aborted for a game that never really started. status is the source's own word for how the game ended and is more precise: mate, resign, outoftime, stalemate, draw, timeout, aborted, noStart, cheat, variantEnd.

timeControl is written the way chess players read it: minutes plus increment in seconds. 3+0 is three minutes and no increment, 0.25+0 is an ultrabullet game of 15 seconds, and a correspondence game reads 2 days/move. clockInitial (seconds) and clockIncrement carry the same numbers unrounded.

Scheduling

onlyNew remembers the game ids a run wrote in a named key-value store of your account (lichess-seen-games-<taskId>, or lichess-seen-games for runs started by hand) — one store per task, so two schedules watching two accounts do not blind each other. Combine it with sinceHours and a small maxItems: each run then costs a handful of rows and returns only what is new. Only games that were really written and charged are remembered: a run cut short by your spending limit or by the timeout leaves the rest for the next run instead of marking it as seen.

With sortDescending: false, onlyNew walks a career forwards: the same store keeps, per account and filter set, the start time of the newest game written so far, and the next run starts its stream there. Without onlyNew, the oldest games are simply returned again on every run.

Examples

The newest rated fast games of one account

{ "mode": "games", "usernames": ["DrNykterstein"], "perfTypes": ["bullet", "blitz"], "rated": "rated", "maxItems": 20 }

Games with moves, clocks and accuracy, ready for an engine or a model

{ "mode": "games", "usernames": ["cutemouse83"], "perfTypes": ["blitz", "rapid"], "rated": "rated",
"includeMoves": true, "includeClocks": true, "includeAccuracy": true, "maxItems": 15 }

Everything played in the last two weeks, for a schedule

{ "mode": "games", "usernames": ["cutemouse83"], "sinceHours": 336, "onlyNew": true, "includeMoves": false, "maxItems": 20 }

One colour of one season, for opening work

{ "mode": "games", "usernames": ["thibault"], "color": "white", "perfTypes": ["rapid", "classical"],
"dateFrom": "2026-01-01", "dateTo": "2026-07-01", "sortDescending": false, "maxItems": 100 }

A ratings table of several accounts

{ "mode": "player", "usernames": ["DrNykterstein", "cutemouse83", "thibault"], "maxItems": 10 }

Five years of blitz, rapid and puzzle ratings of three accounts, for one chart

{ "mode": "ratingHistory", "usernames": ["thibault", "penguingim1", "nihalsarin2004"],
"perfTypes": ["blitz", "rapid", "puzzle"], "sinceDays": 1825, "maxItems": 90 }

The final table of an arena tournament

{ "mode": "tournament", "tournamentKind": "arena", "tournamentIds": ["ayELljKv"], "maxItems": 30 }

The current blitz top 20

{ "mode": "leaderboard", "leaderboardPerf": "blitz", "topCount": 20, "maxItems": 20 }

Output

One row per game, account, rating point, standing place or top-list place. Every row carries url and fetchedAt (UTC, ISO-8601); the lookup modes also carry found and notFoundReason. Every timestamp the source publishes as epoch milliseconds is written as a …Z string.

A games row

Real row from cloud run W0CoC50MMEb0o3ZBi (move list shortened with …):

{
"gameId": "kAdOQKeh",
"player": "DrNykterstein",
"found": true,
"color": "black",
"opponent": "respects_55",
"opponentRating": 2644,
"opponentRatingDiff": -5,
"opponentTitle": null,
"playerRating": 3145,
"playerRatingDiff": 8,
"playerTitle": "GM",
"result": "win",
"winner": "black",
"status": "resign",
"rated": true,
"variant": "standard",
"perfType": "blitz",
"speed": "blitz",
"timeControl": "3+0",
"clockInitial": 180,
"clockIncrement": 0,
"daysPerTurn": null,
"openingEco": "B02",
"openingName": "Alekhine Defense: Sämisch Attack",
"openingPly": 5,
"movesCount": 136,
"moves": "e4 Nf6 e5 Nd5 Nc3 Nxc3 dxc3 d6 Nf3 Nc6 Bb5 a6 Bxc6+ bxc6 O-O f6 exf6 exf6 Nd4 Qd7 Qh5+ g6 …",
"clocks": null,
"evals": null,
"analysed": true,
"playerAccuracy": 93,
"opponentAccuracy": 89,
"playerAcpl": 18,
"opponentAcpl": 24,
"playerInaccuracies": 2,
"playerMistakes": 3,
"playerBlunders": 1,
"opponentInaccuracies": 3,
"opponentMistakes": 3,
"opponentBlunders": 2,
"tournamentId": "mzOPeKWa",
"swissId": null,
"createdAt": "2026-04-08T19:39:03.033Z",
"lastMoveAt": "2026-04-08T19:45:13.708Z",
"durationSecs": 371,
"url": "https://lichess.org/kAdOQKeh",
"fetchedAt": "2026-09-30T22:22:22.545Z",
"notFoundReason": null
}
FieldTypeMeaning
gameIdstringId of the game; url is lichess.org/<gameId>
playerstringThe account this row is written for, spelled as the source spells it
colorstringwhite or black — the colour that account had
opponentstringThe other account, or Stockfish level N for a game against the engine, or Anonymous
opponentRating, playerRatingnumberRatings at the start of the game
opponentRatingDiff, playerRatingDiffnumberRating change from this game; empty for casual games
opponentTitle, playerTitlestringChess title (GM, IM, FM, LM, …) where the account has one
resultstringwin, loss, draw, aborted — from player's point of view
winnerstringwhite, black, or empty for a draw
statusstringHow the game ended; see Result and status values
ratedbooleanRated games moved the rating
variantstringstandard or the variant key
perfTypestringThe rating pool the game counted for — the speed for standard chess, the variant otherwise
speedstringThe time class, also for variant games
timeControlstringMinutes + increment, e.g. 3+0; 2 days/move for correspondence
clockInitial, clockIncrementnumberSeconds
daysPerTurnnumberCorrespondence games only
openingEco, openingName, openingPlystring, string, numberThe opening as the source classifies it. Empty for very short games and some variants
movesCountnumberHalf-moves; empty when includeMoves is off
movesstringSpace-separated standard algebraic notation
clocksnumber[]Remaining time after each move, centiseconds, in move order. Only with includeClocks
evalsobject[]Per move: eval (centipawns), mate, best, judgment. Only with includeEvals, and only for analysed games
analysedbooleanA full computer analysis exists — the flag to filter on before trusting the accuracy columns
playerAccuracy, opponentAccuracynumberAccuracy percentage from the server analysis
playerAcpl, opponentAcplnumberAverage centipawn loss
playerInaccuracies/Mistakes/Blunders, opponent…numberCounts from the analysis
tournamentId, swissIdstringSet when the game was played in an arena or a swiss
createdAt, lastMoveAtstringStart and last move, UTC
durationSecsnumberWall-clock length of the game

A player row

Real row from cloud run MFYEkGauQTaOEeRGc:

{
"username": "DrNykterstein",
"userId": "drnykterstein",
"found": true,
"playerTitle": "GM",
"patron": true,
"countryCode": null,
"fideRating": null,
"ratingUltraBullet": 2406,
"gamesUltraBullet": 92,
"ratingBullet": 3243,
"gamesBullet": 9583,
"ratingBlitz": 3153,
"gamesBlitz": 606,
"ratingRapid": null,
"gamesRapid": 0,
"ratingClassical": null,
"gamesClassical": 0,
"ratingCorrespondence": null,
"gamesCorrespondence": 0,
"ratingPuzzle": null,
"puzzleAttempts": null,
"variantRatings": { "chess960": { "rating": 2541, "games": 129 }, "atomic": { "rating": 2160, "games": 25 } },
"bestPerfType": "bullet",
"bestRating": 3243,
"gamesAll": 10450,
"gamesRated": 10435,
"wins": 7207,
"losses": 2409,
"draws": 834,
"winRate": 69,
"playTimeHours": 324.9,
"createdAt": "2018-12-06T12:44:50.814Z",
"seenAt": "2026-08-28T10:08:45.437Z",
"online": null,
"streaming": false,
"closed": false,
"url": "https://lichess.org/@/DrNykterstein",
"fetchedAt": "2026-09-30T22:22:27.724Z",
"notFoundReason": null
}
FieldTypeMeaning
username, userIdstringThe account as spelled, and its lower-case id
playerTitle, patronstring, booleanChess title; whether the account supports the site
countryCode, fideRatingstring, numberOnly where the player published them on the profile
rating<Speed>numberRating for UltraBullet, Bullet, Blitz, Rapid, Classical, Correspondence. Empty when the account never played that speed — the source reports a provisional starting value there, which is not a rating
games<Speed>numberGames in that speed; 0 is the honest answer for "never played"
ratingPuzzle, puzzleAttemptsnumberThe puzzle rating is a different scale and is kept out of bestRating
variantRatingsobject{ variantKey: { rating, games } } for the variants the account has actually played; empty otherwise
bestPerfType, bestRatingstring, numberThe strongest rating among the speeds and variants played
gamesAll, gamesRatednumberTotal and rated game counts
wins, losses, draws, winRatenumberCounts and wins as a percentage of decided games, one decimal
playTimeHoursnumberTotal time spent playing
createdAt, seenAtstringJoin date and last time the account was seen, UTC
online, streaming, closedbooleanLive flags; closed is set for a disabled account

A rating-history row

Real row from cloud run OabdaaIOJTXNIVYUa:

{
"username": "thibault",
"found": true,
"perfType": "blitz",
"perfName": "Blitz",
"date": "2026-09-30",
"rating": 1742,
"ratingChange": 4,
"url": "https://lichess.org/@/thibault",
"fetchedAt": "2026-09-30T23:03:47.460Z",
"notFoundReason": null
}
FieldTypeMeaning
perfType, perfNamestringThe curve as a key (kingOfTheHill) and as the source names it (King of the Hill)
datestringYYYY-MM-DD in UTC. The source stores the month zero-based; that is normalised here
ratingnumberRating at the end of that day
ratingChangenumberDifference to the previous point of the same curve. Computed over the whole history, so the first row of a window is a real change, not empty

The source records one point per day on which the rating moved, so gaps are days without rated games, not missing data.

A tournament row

Real row from cloud run ePCEJUA5Q5iS8SCj8:

{
"tournamentId": "ayELljKv",
"tournamentKind": "arena",
"tournamentName": "Hourly Bullet Arena",
"found": true,
"nbPlayers": 320,
"perfType": "bullet",
"variant": "standard",
"rated": true,
"timeControl": "1+0",
"clockInitial": 60,
"clockIncrement": 0,
"startsAt": "2026-09-30T21:00:15.000Z",
"durationMinutes": 27,
"createdBy": "lichess",
"rank": 1,
"username": "HalThaPal",
"playerTitle": null,
"score": 36,
"points": null,
"tieBreak": null,
"performance": 2205,
"rating": 2437,
"sheetScores": null,
"fire": null,
"absent": null,
"url": "https://lichess.org/tournament/ayELljKv",
"fetchedAt": "2026-09-30T22:22:31.260Z",
"notFoundReason": null
}
FieldTypeMeaning
tournamentId, tournamentKind, tournamentNamestringThe event, repeated on every row so the dataset reads on its own
nbPlayers, perfType, variant, ratednumber, string, string, booleanEvent properties
timeControl, clockInitial, clockIncrementstring, number, numberThe clock of the event
startsAt, durationMinutes, createdBystring, number, stringStart in UTC; arena length; the account that created it
rank, username, playerTitle, ratingnumber, string, string, numberThe place in the standing
scorenumberArena score, or the swiss points — filled for both formats
points, tieBreaknumberSwiss only
performancenumberTournament performance rating. Filled when the standing is read as a stream; when the actor has to fall back to the paged standing, only the podium has it and the status message says so
sheetScores, firestring, booleanArena streak sheet, when the paged standing is used
absentbooleanWithdrew or did not appear

A leaderboard row

Real row from cloud run L0hHlFgGu7BBBb9Vr:

{
"perfType": "blitz",
"rank": 1,
"username": "Tuzakli_Egitim",
"userId": "tuzakli_egitim",
"playerTitle": "FM",
"rating": 2984,
"progress": 11,
"online": false,
"patron": false,
"url": "https://lichess.org/@/Tuzakli_Egitim",
"fetchedAt": "2026-09-30T22:22:30.993Z"
}
FieldTypeMeaning
perfType, rankstring, numberWhich list, and the place in it
username, userId, playerTitlestringThe account
rating, progressnumberCurrent rating and its recent movement
online, patronbooleanLive flags

A row that found nothing

An account or tournament id the source does not know — and an account whose rating history the source withholds (Limits) — produces a row instead of a silent empty run:

{ "found": false, "notFoundReason": "no Lichess account named \"almatyy_chess\" — the account does not exist or was renamed",
"username": "almatyy_chess", "url": "https://lichess.org/@/almatyy_chess", "fetchedAt": "2026-09-30T22:00:00.000Z" }

The second case, real row from cloud run OabdaaIOJTXNIVYUa — the account exists and plays, the source did not hand over its curve on those three attempts:

{
"found": false,
"notFoundReason": "lichess.org published no rating history for this account although it has 75502 rated game(s) (asked 3 time(s), every answer 200 with an empty list) — the endpoint does this for part of the accounts at a time and the same account often answers minutes later, so repeat the run; mode \"player\" returns the current rating of every speed straight away",
"fetchedAt": "2026-09-30T23:03:47.460Z",
"username": "penguingim1",
"url": "https://lichess.org/@/penguingim1",
"perfType": null, "perfName": null, "date": null, "rating": null, "ratingChange": null
}

The same run wrote 30 real rating points for the first of its three accounts, so one withheld curve costs one row, not the run.

Dataset views and the SUMMARY record

Five views, one per mode: Games, Players, Rating history, Tournament standings, Leaderboard. Open the one named after your mode; the others stay empty for that run.

Every run also writes a SUMMARY record into the default key-value store (linked from the run's output as Run summary): mode, the filters as sent, the number of requests and of 429 answers, rows read, rows written, rows per account, accounts or ids that were not found or not read, how many games were dropped by analysedOnly, how many were already known to onlyNew, the source errors, the warnings, and stoppedBy (limit, timeout or aborted) when the run ended early.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~lichess-games/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"games","usernames":["DrNykterstein"],"perfTypes":["blitz"],"rated":"rated","maxItems":20}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/lichess-games').call({
mode: 'games',
usernames: ['cutemouse83'],
sinceHours: 168,
fields: ['gameId', 'opponent', 'result', 'openingName', 'createdAt'],
maxItems: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/lichess-games").call(run_input={
"mode": "ratingHistory", "usernames": ["thibault"], "perfTypes": ["blitz"], "sinceDays": 365, "maxItems": 400,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items

fields is what makes this usable as a tool for an agent: a games row has more than forty columns, and ["gameId", "opponent", "result", "openingName", "movesCount", "createdAt"] is usually all a model should see. One call answers one question, and the rows are flat JSON with no nesting to walk.

MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/lichess-games tool with the same JSON input.

Pricing

Pay per event: $0.001 per run start + $0.001 per dataset row. Every run is charged the start event, including a run that finds nothing.

RunRowsCost
One account looked up in player mode1$0.001 + $0.001 = $0.002
The 50 newest games of two accounts (the default)50$0.001 + $0.050 = $0.051
A year of blitz rating points for a chart300$0.001 + $0.300 = $0.301
The top 200 of one speed200$0.001 + $0.200 = $0.201

maxItems is the brake: it is a hard cap on the rows written, so it is also a hard cap on the bill. Apify plan discounts apply to the row price — 10 % on Bronze, 20 % on Silver, 30 % on Gold and above — while the start event costs the same on every plan. The 50-row run above therefore costs $0.036 on a Gold plan.

Platform usage is included in these prices — there is no compute or proxy charge on top. The runs behind this README finished in 3 to 30 seconds each, the long ones only because the source insists on one request at a time.

Your maximum cost per run is respected exactly: the run writes only the rows it pays for and ends with the status "Stopped at your spending limit: N rows delivered", so a cap never leaves you with rows charged but missing.

Limits & FAQ

  • One request at a time. The Lichess API asks third-party applications to send one request at a time and, after a 429, to wait a full minute before asking again. This actor therefore serialises every call, pauses between them, and after a 429 waits a full minute (longer if the answer's Retry-After says so) before its next request of any kind — the tournament fallback included. Consequence: ten accounts take about ten times as long as one. It also means you should not start ten runs of this actor in parallel from the same account.
  • Long exports are streamed. The games export arrives at about 20 games per second, so 2,000 games take around 100 seconds. Rows are written as the games arrive; only a stall (no data for 30 seconds) counts as an error, and a stream that breaks off is resumed from the last game received.
  • Timeout and spending limit. Shortly before the run's timeout the actor stops starting new requests, saves what it has read and ends as a success with "Stopped before the run timeout: N rows saved". At your maximum cost per run it ends with "Stopped at your spending limit: N rows delivered". Either way the SUMMARY names what was not read.
  • A very active player has tens of thousands of games. maxItems is the only brake, and in games mode the cap is split evenly over usernames — 60 rows over three accounts is about 20 games each, not 60 games of the first one. Accounts with fewer games leave their share to the ones that follow.
  • Clocks, evaluations and accuracy exist only where the game has them. Most bullet games were never analysed by the server, so playerAccuracy, playerAcpl and the blunder counts are empty for them; the analysed column says which rows to trust. includeEvals combined with analysedOnly avoids paying for rows that come back empty.
  • Ratings of speeds never played are empty on purpose. The source reports a provisional starting value (1500, or 2500 for a strong new account) with games: 0. Writing that as a rating would invent data, so the column stays empty and the count tells the story.
  • The rating history's zero-based month is normalised for you. The source encodes a point as [year, monthIndex0, day, rating], so [2017, 3, 10, 1380] is 2017-04-10. Ports that miss this are off by a month.
  • The rating history is the one mode the source does not always answer. Measured on 2026-09-30 over twelve well-known accounts and two networks: the rating-history endpoint replies 200 with an empty list for part of the accounts at any given moment — accounts with 10 000, 19 000 and 57 000 rated games among them — while other accounts asked in the same minutes return their full curves. No 429 and no Retry-After come with it, so it is not a rate limit and not something a caller can avoid; it is also not stable. One account answered with an empty list seven times in a row over an hour and then returned 32 kB of curves; another answered 148 kB to one network and an empty list to another four minutes later; a third stayed empty all evening. It behaves like a cache on the source's side that can hold an empty value for a while. What the actor does about it: an empty answer is indistinguishable from "this account never played rated", so it asks the profile how many rated games the account has, and if there are any it asks the history again after 2 and 6 seconds. If it is still empty, the run writes one row with found: false and that explanation — it does not claim the account has no rated games, and it does not fail. The other accounts of the same run keep their curves, and you pay the start event plus that one row. The two things that do work: run it again later (often minutes are enough), and mode: "player", which returns the current rating of every speed and always answers. Naming two or three usernames in one ratingHistory run is the practical hedge, which is why the example task does.
  • Correspondence and puzzles have no top list. That is why leaderboardPerf has 13 values and not 15.
  • Arena standings are read as a stream when the source allows it. That path carries the tournament performance rating for everyone. When it is rate-limited, the actor falls back to the paged standing (10 rows per request), where only the podium has a performance — the status message says which path was used.
  • Swiss tournaments produce points and tieBreak instead of an arena score, and no perfType: the swiss endpoint does not publish the rating pool the arena endpoint does. The clock, the variant and the player count are there.
  • Private and imported games are not visible. The export shows what the account's own privacy settings publish; games imported into a study and games an owner has hidden need that owner's personal token, which this actor does not use and does not ask for.
  • Public data only. No login, no captcha, no anti-bot circumvention, no proxy. The free-text profile (real name, biography, links) is not written to any row and there is no contact field in the source to read.
  • Empty results versus errors. An account or tournament id the source does not know produces a found: false row, and so does an account whose rating history the source withholds. In games mode a filter combination that matches nothing produces no rows and a status message naming the filter that emptied the run. An input that cannot be read at all — no usernames in a mode that needs them, dateTo before dateFrom — fails immediately with the reason, before any request. A run fails otherwise only when it wrote nothing and the source was the reason: unreachable after the backoff, or a 429 that outlasted it. A run that wrote some rows always ends as a success, with the accounts that went wrong named in the status message and listed under sourceErrors in the SUMMARY record.
  • If Lichess changes or restricts the export, this section will say the actor is paused. Working around a restriction is not something this actor does.

Made by Yadroo. Sibling actors: arxiv-papers · openalex-works · wikipedia-search · hackernews-search · stackexchange-search