> Fetch the complete index of this site at https://folk.lol/llms.txt, or all of it in one file at https://folk.lol/llms-full.txt. Use it to discover every page before exploring further.

# FOLK bot API - play folk.lol from code

> Bots can play FOLK now. Write one in a few lines of JavaScript, or point an AI agent at it - it plays the live world at human pace, like everyone else.

- Explore the overworld, fight wild Folk, gather, dig, eat and build a shelter.
- Run a dungeon alone: win the Warden's Boss Key, go down and beat the boss.
- Travel to the town, buy spells at its Spells Shop and teach them to your Folk.
- Take the coins and items your owner gifts you.
- Emote and send quick-chat phrases to the players near you, and see theirs.
- Play at human pace: every step and action takes its time in the game.
- Stay out of islands, the town's other shops, fishing and co-op dungeon runs.
- Carry a bot label, and belong to a player as their own account with their own Folk.

## Why bots

We built the bot API to make FOLK better. A bot plays for hours without getting bored, so we run our own and watch them: where they run out of energy, what they can't reach, which fights never pay. What we learn, we change in the game, for everyone.

Now bring yours. Teach a Folk to farm, fight and run dungeons, or point an AI agent at it: a persistent world with real rules and long goals is a good place to test one. Play nice - your bot shares the world with people.

The story: [Bots can play FOLK now](https://folk.lol/news/bots-can-play-folk)

## Get a key

In the game (https://folk.lol/game): Settings -> My account -> Bots. A bot key is shown once; if it leaks, Actions -> Replace key gives the same bot a new one.

[Bot API terms](https://folk.lol/terms#bot-api): using the API means agreeing to them. You answer for your bots, fan projects stay non-commercial, and nothing may give Folk or items real-world value.

Each bot has permissions, switched with the 🔐 beside it there: `dungeon` lets it enter dungeon runs, and a new bot has it. `GET /v1/me` lists yours; a call they don't allow answers 403 `forbidden`, naming the `permission`.

## Hello, world

Send the key as `Authorization: Bearer folkbot_...`. Your first request puts your Folk in the world; then the event stream tells you what happens to it.

Run it with `FOLK_BOT_KEY=folkbot_... node hello.mjs`. Nothing happens to a bot that stands still: send it a gift from My account -> Bots to see an event arrive.

`hello.mjs`:

    const API = "https://api.folk.lol/v1";
    const headers = { authorization: `Bearer ${process.env.FOLK_BOT_KEY}` };
    
    const state = await fetch(`${API}/state`, { headers });
    const { me, error } = await state.json();
    if (!state.ok) throw new Error(error); // a wrong key, or the API closed
    console.log(`Hello, world! I'm at ${me.x}, ${me.y}`);
    
    const events = await fetch(`${API}/events`, {
      headers: { ...headers, accept: "text/event-stream" },
    });
    
    let buffer = "";
    for await (const chunk of events.body.pipeThrough(new TextDecoderStream())) {
      buffer += chunk;
      const lines = buffer.split("\n");
      buffer = lines.pop();
    
      for (const line of lines) {
        if (line.startsWith("data: ")) console.log(line.slice(6));
      }
    }

## Endpoints

The same as OpenAPI 3.1, to generate a client from: https://api.folk.lol/v1/openapi.json

### You

`GET /v1/state` `{ include? } => State`

You and what you see. Each POST's answer carries it too, as `state`, with the `events` since the last answer that carried them. As `text/event-stream`: the whole State, then the changed keys every 250 ms; keep the higher `seq`.

    // GET /v1/state as a stream: a whole State first, then
    // { seq, ...each top-level key that changed }
    export interface State {
      seq: number;              // higher is newer
      me: {
        x: number;
        y: number;
        hp: number;
        maxHp: number;
        level: number;
        xp: number;
        xpToNext: number;
        folkId: number;
        energy: number;
        energyRefillAt: number; // epoch ms; 0 if full
        dead: boolean;
        spells: string[];       // MetaSpell.key
      };
      coins: number;
      backpack: Item[];
      weight: number;
      world: {
        kind: "overworld" | "town" | "dungeon";
        width: number;
        height: number;
      };
      enemies: Enemy[];
      players: Player[];
      ground: { x: number; y: number; item: string }[];
      graves: {                // bots' graves only: bots and humans never loot each other's
        x: number;
        y: number;
        items: Item[];
        expiresAt: number;      // epoch ms
      }[];
      buildings: Placed[];      // yours
      gifts: Gift[];
      dungeon: Dungeon | null;
      dig: {                    // the board a dig opened
        w: number;
        h: number;
        tapsLeft: number;
        cells: { x: number; y: number; found: Found }[];
      } | null;
    }
    
    export interface Item {
      key: string;
      count: number;
    }
    
    export interface Enemy {
      id: string;
      folkId: number;           // 0: friendly, not a fight
      level: number;
      hp: number;
      maxHp: number;
      chasing: boolean;         // after a player
      x: number;
      y: number;
      boss?: {                  // covers w x h tiles from x, y
        key: string;            // "warden": holds the Boss Key
        w: number;
        h: number;
        armored: boolean;       // hits do nothing now
      };
    }
    
    export interface Player {
      id: string;
      name: string;
      folkId: number;
      level: number;
      x: number;
      y: number;
      bot: boolean;
    }
    
    export interface Placed {
      key: string;
      x: number;
      y: number;
    }
    
    export interface Gift {
      id: number;
      from: string;             // their name
      fromId: string;           // their Player id
      coins: number;            // 0: an item gift
      item: Item | null;        // null: a coin gift
      at: number;               // epoch ms
    }
    
    export interface Dungeon {
      floor: 1 | 2;             // 2: the boss's
      width: number;
      height: number;
      lit: boolean;             // no fog: seen holds every tile
      exit: { x: number; y: number };
      descent: { x: number; y: number } | null;
      keyHeld: boolean;         // the descent opens for you
      seen: DungeonTile[];
      traps: { x: number; y: number }[];
      hazards: {                // a boss's moves
        warn: { x: number; y: number }[];  // bite next
        hit: { x: number; y: number }[];   // biting now
        drain: boolean;         // heals the boss off you
        since: number;          // epoch ms
      };
      looks?: DungeonLook[];    // only with ?include=looks
    }
    
    export type Found = "empty" | "coin" | "item" | "rock";
    
    export interface DungeonTile {
      x: number;
      y: number;
      tile: "wall" | "floor" | "corridor" | "room" | "spawn"
        | "exit" | "trap" | "loot" | "descent";
      look?: number;            // into looks
    }
    
    export interface DungeonLook {
      bg: string;
      layers: {
        frame: string;
        rotate?: number;        // degrees
        scale?: number;
      }[];
      tint?: { color: string; alpha: number };
      shade: number;
      warn?: { color: string; alpha: number };
      hit?: string;             // image URL
    }

`GET /v1/events` `{ since? } => Events`

What happens to you and around you. As `text/event-stream`: each event with its `id`; reconnect with `Last-Event-ID` to catch up. As JSON: the events after `?since=` (the last answer's `last`), waiting up to 25s for one.

    export interface Events {
      events: Logged[];         // after `since`, oldest first
      last: number;             // the next `since`
      resync: boolean;          // some were lost: read /v1/state
    }
    
    export interface Logged {
      id: number;
      event: Event;
    }
    
    // What happened to you, oldest first; dealt and took are a fight's totals
    export type Event =
      | { type: "attackedBy"; enemy: string }
      | { type: "won"; enemy: string; dealt: number; took: number }
      | { type: "earned"; xp: number; coins: number; reputation: number } // 0 xp, 0 coins: you out-level it
      | { type: "lost"; enemy: string; dealt: number; took: number }
      | { type: "ended"; enemy: string; dealt: number; took: number } // it's gone: fell to someone else, left, or you walked off
      | { type: "died" }
      | { type: "dropped"; item: Item }
      | { type: "lostCoins"; coins: number }
      | { type: "levelUp"; level: number }
      | { type: "hurt"; damage: number }   // no fight: a trap, a boss's move, a burn
      | { type: "entered"; floor: number } // a dungeon
      | { type: "floor"; floor: number }   // went down
      | { type: "key" }                    // the Warden dropped the Boss Key
      | { type: "escaped" }                // walked out of a dungeon
      | { type: "travelled"; to: "town" | "overworld" }
      | { type: "gift"; gift: Gift }       // also in state.gifts
      | { type: "folks" }                  // your roster changed: GET /v1/folks
      | { type: "bumped"; by: string; byId: string; bot: boolean } // a player walked into you; once per step held into you
      | { type: "chat"; from: string; fromId: string; bot: boolean; emote?: string; phrase?: string } // a player you see, as POST /v1/chat; bot: another bot sent it
      | { type: "resync" };                // GET /v1/events only: some were lost - read /v1/state

`POST /v1/respawn` `{} => Respawned`

Come back after falling: at your shelter or by the portal; from a dungeon, outside it.

    export interface Respawned extends Answer {
      at: "shelter" | "portal" | "entrance" | null; // entrance: out of a dungeon
    }
    
    // Every POST's answer: what it did, then these
    export interface Answer {
      events: Event[];          // since your previous answer
      state: State;
    }

`GET /v1/me` `{} => Profile`

Your bot's profile. Doesn't join the world.

    export interface Profile {
      name: string;
      description: string | null;
      createdAt: string;        // ISO
      permissions: string[];    // what your owner lets you do: dungeon
    }

`PUT /v1/me` `{ name?, description? } => Profile`

Edit your name or description (`null` clears it); a new name shows from your next join.

`DELETE /v1/session` `{} => Ok`

Leave now, not after 2 idle minutes.

    // DELETE /v1/session
    export interface Ok {
      ok: true;
    }

### Viewport

`PUT /v1/viewport` `{ w, h } => Viewport`

Resize your viewport, up to 64x64 tiles (default 25x25), and get its tiles as GET does. It follows you. Not in a dungeon.

    // Each distinct tile once, and a grid of indexes into it
    export interface Viewport {
      x0: number;
      y0: number;
      palette: Tile[];
      grid: (number | null)[][]; // [y - y0][x - x0] into palette; null: off the map
    }
    
    export interface Tile {
      key: string;
      type: string;
      id: number;
      variant: number;
      altitude: number;
      walkable: boolean;
      ready?: boolean;
      building?: string;
      mine?: boolean;
    }

`GET /v1/viewport` `{} => Viewport`

Your viewport's tiles: what each is, whether you can stand on it, whether it's ready to harvest, and any building on it (`mine` if it's yours). In a dungeon, `state.dungeon` has the floor instead.

### Moving

`POST /v1/move` `{ steps: [{ dx, dy }], stopOn? } => Moved`

Up to 32 steps (8 in a dungeon, 1 on its boss's floor), each one tile along `dx` or `dy`. Into an enemy: attack. Stops at a step not taken, or after an event of a type in `stopOn` (say `["attackedBy", "hurt"]`).

    export interface Moved extends Answer {
      moved: boolean;           // the last step went through
      blocked: Blocked | null;  // why the last step didn't
      attacked: {
        id: string;
        damage: number;         // 0: too soon after your last
                                // hit, or it's armored
        hp: number;             // the enemy's
      } | null;
      steps: number;            // how many went through
      stoppedBy: Event | null;  // the first event of a stopOn kind
      gained: Item[];           // walked over
    }
    
    export type Blocked =
      | "dead"
      | "energy"
      | "edge"
      | "obstacle"              // water, rock
      | "forest"                // a tree walled in by trees
      | "cliff"                 // a climb of 2+ levels
      | "building"
      | "wall"                  // a dungeon's
      | "occupied"
      | "busy";

### Actions

`POST /v1/action` `{ action, building?, map?, spell? } => Acted`

Act on your tile, as it offers: `harvest`, `cut`, `dig`, `build` (`building: "shelter"`), `dismantle`, `ko` (back home), `enter_dungeon` (`map` to pick one), `travel_town`, `travel_overworld`, `spells` (`spell` to buy); in a dungeon `descend` and `leave_dungeon`. Answers when done.

    export interface Acted extends Answer {
      ok: boolean;
      reason: Reason | null;
      gained: Item[];
      tile: { key: string; variant: number } | null;
      busyMs: number;
      board?: { w: number; h: number; taps: number }; // dig
      spent?: Item[];           // build
      building?: Placed | null; // build
      replaced?: Placed | null; // build
      missing?: Item[];         // build
      paid?: number;            // enter_dungeon, spells: coins
    }
    
    // The game's key for why something came to less or nothing
    export type Reason =
      | "packFull"
      | "storageFull"
      | "notEnoughEnergy"
      | "notEnoughCoins"
      | "notEnoughResources"    // build: see missing
      | "notEnoughArtifacts"    // summon
      | "nothingToHarvest"
      | "onCooldown"
      | "maxLevel"
      | "needsWater"
      | "wrongWater"
      | "needsKey"              // descend: the Boss Key
      | "alreadyDugHere"
      | "dugNothing"
      | "unearthedRock"
      | "buildTileBlocked"
      | "buildTooClose"
      | "depositExists"
      | "aquariumExists"
      | "notYourBuilding"
      | "unknownFolk"
      | "spellNotOwned"
      | "folkSpellsFull"        // teach: name one to replace
      | "folkKnowsSpell"
      | "giftInboxFull";

`POST /v1/action/dig/tap` `{ x, y } => Dug`

Uncover a cell of the board a `dig` opened: `x`, `y` from its top-left corner.

    export interface Dug extends Answer {
      x: number;                // the cell you tapped
      y: number;
      found: Found;
      item: string | null;
      piece: {                  // this cell in the buried shape's w x h box
        ox: number;
        oy: number;
        w: number;
        h: number;
      } | null;
      tapsLeft: number;
      done: boolean;
      gained: Item[];
      coinsFound: number;
      reason: Reason | null;
    }

### Folk

`GET /v1/folks` `{} => Roster`

Every Folk you own, and which one you play.

    export interface Roster {
      activeUid: string;        // the Folk you play: state.me
      folks: {
        uid: string;
        folkId: number;
        level: number;
        xp: number;
        hp: number;
        maxHp: number;
        spells: string[];       // MetaSpell.key
      }[];
    }

`POST /v1/folk/summon` `{ folkId } => Summoned`

Summon a Folk, paying its `summonCost` in artifacts from your backpack and storage. Only `summonable` ones (GET /v1/meta/folks).

    export interface Summoned extends Answer {
      uid: string;              // Roster.folks[].uid
    }

`POST /v1/folk/select` `{ uid } => Answer`

Play another of your Folks. Not in a dungeon.

`POST /v1/folk/teach` `{ spell, replace? } => Taught`

Teach the Folk you play a spell you own, from your backpack or storage (used up). It knows up to 4; when full, `replace` one.

    export interface Taught extends Answer {
      replaced: string | null;  // MetaSpell.key
    }

### Chat

`POST /v1/chat` `{ emote?, phrase? } => Answer`

Show an emote (`emote`: its key or emoji) or a quick-chat phrase (`phrase`: its key) above you, to everyone near; GET /v1/meta/game lists both. Two at once, then about one every 1.5s. What players you see send comes as `chat` events; `bot` marks another bot's, so two bots that answer each other don't loop.

### Inventory

`POST /v1/inventory/eat` `{ item } => Ate`

Eat one of a food in your backpack (`food` in GET /v1/meta/items): it heals, refills energy or boosts.

    export interface Ate extends Answer {
      healed: number;
      energyGained: number;
      boost: (Boost & { key: string; until: number }) | null;
    }
    
    export interface Boost {    // a dish's, for 15s
      attackPct?: number;
      defensePct?: number;
      specialAttackPct?: number;
      speedPct?: number;
      luck?: number;
    }

`POST /v1/inventory/drop` `{ item, count? } => Dropped`

Throw up to 20 (default 1) of an item in your backpack away for good. A fish only into water it lives in. Not in a dungeon.

    export interface Dropped extends Answer {
      dropped: number;
      reason: Reason | null;
    }

`POST /v1/inventory/take` `{ item?, count? } => Taken`

From the bot grave you stand on, take up to 20 of `item` - or of anything, lightest first - as many as fit. Not in a dungeon.

    export interface Taken extends Answer {
      taken: Item[];
      left: Item[];
      reason: Reason | null;
    }

### Gifts

`POST /v1/gifts/accept` `{ id } => Answer`

Take one of `state.gifts`: coins to your wallet, items to your backpack, then storage.

`POST /v1/gifts/decline` `{ id } => Answer`

Send one back.

### Catalog

`GET /v1/meta/folks` `{} => MetaFolks`

Every Folk: stats, drops, summon cost, images. `id` is `folkId`.

    export interface MetaFolks {
      folks: MetaFolk[];
    }
    
    export interface MetaFolk {
      id: number;
      edition: 1 | 2;
      name: string;
      description: string;
      element: string;
      stats: {                  // at level 1
        hp: number;
        attack: number;
        defense: number;
        speed: number;
        special_attack: number;
        luck: number;
      };
      face: "left" | "right";
      growth: "fast" | "medium" | "slow"; // MetaGame.rewards.growth
      drops: string[];          // MetaItem.key
      summonable: boolean;
      summonCost: Item[];       // artifacts, spent from backpack and storage
      image: {
        token: string;          // 150 px
        sprite: string;
        card: string;           // with background
      };
    }

`GET /v1/meta/items` `{} => MetaItems`

Every item: weight, food value, image.

    export interface MetaItems {
      items: MetaItem[];
    }
    
    export interface MetaItem {
      key: string;
      kind: "resource" | "artifact";
      name: string;
      description: string;
      rarity?: string;          // artifacts only
      type?: string;            // resources only
      weight: number;           // per unit
      food?: {                  // edible only
        heals: number;
        energy: number;
        boost: Boost | null;
      };
      image: string;
    }

`GET /v1/meta/spells` `{} => MetaSpells`

Every spell: damage, accuracy, status, shop price, image.

    export interface MetaSpells {
      spells: MetaSpell[];
    }
    
    export interface MetaSpell {
      key: string;
      name: string;
      description: string;
      element: string;
      type: "physical" | "special";
      damage: number;
      accuracy: number;         // 0..1
      status?: {
        effect: string;
        chance: number;         // 0..1
      };
      price: number | null;     // coins; null: not sold
      image: string;
    }

`GET /v1/meta/game` `{} => MetaGame`

Backpack, energy, level cap, grave time, shelter, dungeon fee, kill rewards, element chart, chat emotes and phrases, icons.

    export interface MetaGame {
      capacity: number;         // max State.weight
      maxEnergy: number;
      energyRefillMs: number;   // one point back every
      maxLevel: number;
      graveMs: number;          // how long a fall's grave lasts
      shelterPeaceRadius: number; // tiles from your shelter, straight-line: no enemy starts a fight
      shelterCost: Item[];
      dungeonFee: {             // coins: base + perLevel x your level
        base: number;
        perLevel: number;
      };
      rewards: {                // beating an enemy at or above your level; nothing if you out-level it
        xpPerLevel: number;     // xp = round(its level x xpPerLevel x growth[its MetaFolk.growth])
        growth: Record<string, number>;
        coinsBase: number;      // coins = coinsBase + coinsPerLevel x its level;
        coinsPerLevel: number;  // an overworld fall costs the same at your level
      };
      combat: {
        stab: number;           // damage x when the spell's element is your Folk's
        elements: Record<string, Record<string, number>>; // [attack][defense] damage x; absent: 1
      };
      icons: {
        coins: string;
        energy: string;
      };
      emotes: {                 // POST /v1/chat's `emote`
        key: string;
        emoji: string;
      }[];
      phrases: {                // its `phrase`
        key: string;
        text: string;           // in English; players read it in their language
      }[];
    }

`GET /v1/meta/tiles` `{} => MetaTiles`

Every tile type and its look per variant, by Tile `id` and `variant`.

    export interface MetaTiles {
      tiles: {
        id: number;
        key: string;
        name: string;
        description: string;
        type: string;
        variants: {             // by Tile.variant, clamped
          background: string;
          sprite: string | null;
          reflection?: string;  // water: where (x*73 + y*97) % 20 == 0
        }[];
      }[];
      plantedSeed: { variant: number; sprite: string };
      alpha: { sprite: number; reflection: number };
    }

`GET /v1/meta/sprites` `{} => MetaSprites`

The sprite sheets and each frame's rectangle.

    export interface MetaSprites {
      sheets: string[];
      frames: Record<string, {  // in sheets[sheet]
        sheet: number;
        x: number;
        y: number;
        w: number;
        h: number;
      }>;
    }

`GET /v1/meta/sprites/dungeon` `{} => MetaSprites`

The same for dungeons.

## Rules

- Positions are world tiles: +x east, +y south.
- A step costs 1 energy and a dig 75 energy; 1 energy comes back every 5s. You can't step while harvesting, building or digging.
- Walking into an enemy attacks it; the fight runs on its own until one of you falls. Follow it in events.
- Falling costs coins and drops your backpack as a grave for 2 minutes, which only bots can see and take from. You respawn on your shelter or beside the portal. In a dungeon it ends the run and empties your backpack.
- Step on enemy drops and tree seeds to pick them up. A bot's kill drops for bots only, a human's for humans.
- A shelter costs 4 wood and 2 stone.
- A dig opens a board (its answer's `board`); uncover every cell of the buried item to get it.
- In a dungeon, steps are free and your torch lights 2 tiles round you.
- The portal at the world's centre, or your shelter, takes you to the town and back.
- A Folk fights with the strongest spell it knows.

## Errors

Every error is `{ code, error, reason? }`: a `code` to branch on, by status, and `reason` when the game said why. A POST that timed out is safe to retry with the same body and `Idempotency-Key` header: while your session lives you get its first answer, it isn't played twice. An error isn't kept: the retry plays.

| Status | Meaning |
| --- | --- |
| 400 | `invalid`: malformed, the error says which field. `notOffered`: your tile doesn't offer that action now; `actions` lists what it does, `reason` why an offered one can't happen (`packFull`). |
| 401 | `unauthorized`: key missing, unknown or revoked. |
| 403 | `banned`: this bot may not play. `closed`: the bot API isn't open. `forbidden`: your owner switched off the `permission` it names; GET /v1/me lists yours. |
| 404 | `notFound`: no such endpoint. |
| 409 | `notNow`: not in this state (no dig board open, not fallen, in a dungeon or not). `refused`: the game said no, `reason` says why. `seats`: your bots already play in 2 sessions; close one (DELETE /v1/session) first. |
| 413 | `tooLarge`: the body is over 64 KB. |
| 429 | `rateLimited`: over 10 requests a second (bursts of 20), 5 waiting, 2 streams open, rejoining too often, or too many new keys from one address. Wait Retry-After seconds; every answer's RateLimit-Remaining and RateLimit-Reset say how close you are. |
| 502 | `gameError`: the game didn't answer. Also 504. |
| 503 | `full`: every bot seat is taken. `notSpawned`: your Folk isn't in the world yet (joining, travelling). Try again shortly. |

## Versions

Experimental until the bot API opens: anything may change. Every answer's Folk-Api-Revision header names the contract it was built from; it changes whenever the contract does.

| Date | Version | Change |
| --- | --- | --- |
| 2026-10-09 | v1 | First version. |

## Contact

contact@folk.lol

---

Canonical HTML: https://folk.lol/bots
Sitemap: https://folk.lol/sitemap.xml
