# Fenwild: build a player-run pilot Fenwild is a persistent multiplayer RPG that openly supports automation. This is an integration guide for an agent helping an account owner. Reading it needs no login. Protocol version: 1. ## Entry points and availability - Website, account and guides: https://fenwild.com - Game and SDK baseUrl: https://play.fenwild.com (not the website origin). - Human-readable setup: https://fenwild.com/docs/pilots - API and types: https://fenwild.com/docs/pilots/api - Training: https://fenwild.com/docs/pilots/training - Public world reference: https://fenwild.com/wiki @fenwild/pilot-sdk is not yet published to the npm registry, and game signup availability is shown on the game entry page. Install, run and training commands on these pages apply once it is published. The SDK is ESM for Node.js 22+ or Bun. Production browser connections are allowed only from play.fenwild.com pages. Run an independent bot in Node.js or Bun. No private repository, database, model upload or hosted inference is required. ## Owner setup 1. The owner creates an account and character through the game entry page. 2. Sign in at https://fenwild.com/account#bots, select the intended character, then create a named Bot credential. Its secret is shown once and expires after 365 days. Up to ten active credentials may exist per character. 3. The owner supplies it locally as FENWILD_AUTOMATION_TOKEN. Keep it out of chat, source control and logs. Never substitute an owner browser-session token. 4. Close game tabs controlling that character. Account can stay open. Each character has one controller. All four account characters can play at once, each manually or with its own bot or Pilot. Starting another character's bot leaves this bot running. ## First run After the package is published, prepare a local project: ```sh mkdir my-fenwild-pilot && cd my-fenwild-pilot npm init -y npm install @fenwild/pilot-sdk export FENWILD_AUTOMATION_TOKEN="the-one-time-secret" ``` Save the following as pilot.mjs. This is complete JavaScript: ```js import { runPilot } from "@fenwild/pilot-sdk"; const automationToken = process.env.FENWILD_AUTOMATION_TOKEN; if (!automationToken) throw new Error("Set FENWILD_AUTOMATION_TOKEN first"); const shutdown = new AbortController(); process.once("SIGINT", () => shutdown.abort()); process.once("SIGTERM", () => shutdown.abort()); let firstObservation = true; await runPilot({ baseUrl: "https://play.fenwild.com", automationToken, signal: shutdown.signal, onStatus: (status) => console.log("Pilot:", status), onError: (error) => console.warn(error.code, error.message) }, (observation) => { const { player } = observation.snapshot; if (firstObservation) { console.log("Observing tick", observation.serverTick, "at", player.position); firstObservation = false; } if (!player.runEnabled && player.runEnergy > 0) { return { type: "toggleRun", enabled: true }; } return undefined; }, { onActionRejected: ({ action, error }) => { console.warn(action.type, error.code, error.message); } }); ``` Run `node pilot.mjs` or `bun pilot.mjs`. Expect `Pilot: connected` and a first observed tick and position. The example enables run when possible, then observes quietly. It does not walk, fight or train. Ctrl+C stops it. ## Build a useful decision loop runPilot(options, brain, callbacks?) gives brain(observation, pilot) the current observation. Return one exact PlayerIntentAction or undefined; async brains are supported. Use observation.snapshot.player for position, hometownId, vitals, skills, inventory, equipment, activity and interactionFocusObjectId. The snapshot also carries visible NPCs, ground items, nearby players, shopStock and combat cues. Hidden actors, private player inventories and future rolls are not available. The authoritative tick is 600 ms. An observation older than 10 ticks (6 seconds) is rejected as stale_intent. Bound model latency; with connectPilot, reconsider a slow result against pilot.lastObservation() before submitting it with that newer observation. Simply changing the tick on an old decision does not make the decision current. Await each submit. Acceptance means the server accepted that intent; it may only start a walk, fight or crafting queue. Wait for later snapshots to verify completion. Unread snapshots coalesce to the newest truth. Use one consumer per async feed; multiple iterators compete for entries, rather than receiving broadcasts. The SDK caps pending commands at eight. A pilot has the same action cadence, range, supply, capacity, combat and social validation as ordinary human control. Use parsePilotAction(unknown) for model-generated JSON; it checks shape, not legality. Illustrative action shapes (replace fictional positions and IDs from current observations; do not execute this list as a sequence): - `{"type":"toggleRun","enabled":true}` — Enable run when energy is available and it is not already enabled. - `{"type":"moveTo","position":{"x":2,"y":-4}}` — Replace these fictional coordinates with a known destination. Submit the destination once; watch later positions for arrival. - `{"type":"headToBank"}` — Approach your hometown Bank. Wait for its interaction focus before using its services (see arrival below). - `{"type":"restAtBank"}` — Rest while legally at your Bank; observe recovery. - `{"type":"depositItem","itemId":"healing_potion","quantity":"all"}` — Deposit a carried item at the Bank; quantity is a positive integer or all. - `{"type":"headToShop"}` — Approach your hometown Shop; wait for its interaction focus before buying. - `{"type":"buyShopItem","itemId":"healing_potion","quantity":1}` — Buy from current shop stock at the Shop with enough gold and carrying space. - `{"type":"useItem","itemId":"healing_potion"}` — Use a carried potion while injured, alive and past foodCooldownUntilTick. - `{"type":"attackTarget","targetId":"visible-npc-id"}` — Use a current snapshot NPC's npcId and a legal target. Combat continues after acceptance; do not reissue each tick. - `{"type":"lootGround","groundItemId":123}` — Replace the fictional ID with a current groundItems entry; ownership, range and carrying limits still apply. - `{"type":"gatherResource","resourceNodeId":"visible-resource-id"}` — Use a current resource node's ID when ready and accessible. - `{"type":"headToTradingPost"}` — Approach the hometown Trading Post before transactions. Reading marketTapes() is global. - `{"type":"loadTradingPost"}` — Read the place-bound Trading Post view; serialize Trading Post requests. - `{"type":"postTradingPostBuyOffer","itemId":"healing_potion","quantity":10,"unitPrice":6}` — At the Trading Post, buy any listings at or under that unit price, then escrow crowns for the rest until sellers fill it; collect purchases there. - `{"type":"cancelActivity"}` — Request cancellation of your current activity, subject to ordinary server rules. All current action verbs: moveTo, cancelActivity, closeInteractionFocus, toggleRun, headToBank, headToShop, headToTradingPost, headToCraftingStation, headToKillBoard, talkToArtificeInstructor, restorePrayer, headToWild, attackTarget, toggleRageSpecial, setAttackStyle, setCharacterGender, castSpell, castHealingWinds, useItem, craftRecipe, forgeMasterwork, inspectWeaponChronicle, cancelCrafting, restAtBank, depositItem, depositAll, withdrawItem, buyShopItem, sellShopItem, equipItem, unequipItem, togglePrayer, dropItem, lootGround, restoreAtShrine, gatherResource, sendChatMessage, invitePartyMember, acceptPartyInvite, declinePartyInvite, removePartyMember, leaveParty, issuePartyDirective, acknowledgePartyDirective, loadFriends, addFriend, removeFriend, loadMutes, mutePlayer, unmutePlayer, requestPlayerTrade, openPlayerTrade, offerPlayerTradeItem, removePlayerTradeOffer, acceptPlayerTrade, confirmPlayerTrade, cancelPlayerTrade, loadTradingPost, listTradingPostItem, buyTradingPostListing, cancelTradingPostListing, collectTradingPostExpired, collectTradingPostProceeds, postTradingPostBuyOffer, fillTradingPostBuyOffer, cancelTradingPostBuyOffer, collectTradingPostPurchase. For full required/optional fields, import the re-exported PlayerIntentAction type from @fenwild/pilot-sdk; its declaration ships with the installed package. Do not invent HTTP gameplay endpoints or undocumented action fields. Use @fenwild/pilot-sdk/game-data for public items, services, recipes, maps and knownPathToward. Reference data is fixed at package release; current snapshots and receipts win. World coordinates can be negative. serviceDefinition("bank") names the Fenwild-town Bank; it is not hometown-relative. headToBank without objectId selects the character's own Bank. Enemy territory is dangerous and its services are unavailable. Pilot movement does not present a human entry-confirmation dialog. After a headToBank, headToShop or headToTradingPost approach, arrival is visible in the resulting or a later snapshot: player.interactionFocusObjectId matches the destination service's entityId. If travel is interrupted, inspect position and activity before replanning. Look up bank, frontier_weapon_shop or trading_post for Fenwild; use stonewake_bank, stonewake_frontier_weapon_shop or stonewake_trading_post for a Stonewake character. Bank contents are player.bank (use an empty array when omitted); shop inventory is snapshot.shopStock. These contents can be visible away from the service and do not prove arrival or access. ## Failures and shutdown The SDK first renews the credential through POST /auth/session/renew (a five-second request timeout), then opens the realtime WebSocket (a separate ten-second default deadline). Temporary renewal failures, retryable admission closes and a busy character (character_in_use) receive at most three startup retries with backoff. Other initial handshake failures stop startup; retry later with a bounded supervisor policy. reconnect: false disables these connection retries. The same scoped token authorizes renewal; no identity-provider token is needed. Renewal never extends the token's absolute expiry. fetch and socketFactory are separate custom adapters. Do not use a renewal response as a gameplay observation. - character_in_use: another game tab, device or bot controls this character; close it. A refused pilot is never reported connected. At startup the SDK retries at most three times, then connectPilot and runPilot reject with PilotProtocolError code character_in_use. A pilot's reconnect takes over its own dropped stream at once. A running pilot refused by another controller keeps backing off until the character is free. A second process on the same token takes the character and the first stops with control_superseded, so run one process per token. A connected status alone is not a snapshot. - PilotProtocolError from submit: the action was refused. onActionRejected handles correlated refusals and allows runPilot to continue; omission stops it. - PilotActionUnknownError: confirmation was lost or timed out (ten seconds by default). The action may have happened. Never replay it blindly. runPilot stops by default; advanced onActionInterrupted handlers discard uncertain local work and reconsider fresh truth, without labelling the action accepted or rejected. - PilotCredentialRevokedError: obtain a replacement from the owner after token revocation/expiry or lost account authorization. Never retry the dead credential. - PilotControlSupersededError: authority replaced this character's control. Stop instead of fighting that replacement. Sibling bots run independently. Both terminal errors reach onError. - Once running, a terminal revocation or control_superseded close resolves runPilot; at startup it rejects. Aborting a running pilot also resolves. A supervisor must inspect onError before restarting resolved runs, or it can restart straight into the same terminal close. After establishment, ordinary network drops reconnect with bounded jittered backoff. An uncertain submitted action is never replayed. Pass an AbortSignal into both runPilot and your own model/HTTP calls; SDK shutdown does not cancel arbitrary work owned by your brain. With connectPilot, always close() in a finally block. ## Game rules and starter strategies Bots earn the same normal skill XP as manual play. Bot control is visible to other players. Deaths, item loss, PvP, trade, chat and moderation remain real; the account owner is responsible for the bot's conduct. @fenwild/pilot-sdk/brains exports survivalGuardian and yardTrainer. The guardian only heals with carried supplies. The trainer works in its own hometown while healthy and idle, collecting eligible owned drops and attacking free trainers. It does not navigate from any location, replenish supplies or guarantee progress. Run one at a time with npx --no-install fenwild-survival-guardian or npx --no-install fenwild-yard-trainer. FENWILD_URL optionally selects another server for these CLI tools; the JavaScript SDK uses its explicit baseUrl option. The in-game Game → Resident Pilot choices are separate local Go/WASM brains. Their imports are data-only resident contenders; arbitrary JavaScript modules or the SDK's reference linear model are not Deck imports. Independent SDK bots can use any strategy or compatible model under the ordinary protocol. ## History, replay and learning lastHistory()/historyUpdates() provide bounded owner history; lastMarketTape()/marketTapes() provide the global public tape. Trading remains place-bound. recentActionOutcomes() preserves accepted/rejected/unknown status. events() is reserved and empty today: combatImpacts, pendingProjectiles, healingEvents, chat, killfeed and nearbyDeaths are snapshot fields. PilotRecorder and replayPilotRecording capture/replay local player-visible context, not a simulator of counterfactual actions. Recordings contain private player activity and should be protected. The training CLI's collect and play commands act in the live game; benchmark/train/evaluate and recording conversion are local. Collect independent journeys for train/validation/test splits. The reference model predicts action types among candidates, not legal moves or a proven strategy, and its synthetic benchmark is not a gameplay-quality guarantee. Continue with the linked API and training guides for exact options and workflows.