# PoE2 Build Planner — agent guide This site is a Path of Exile 2 build planner. AI agents are first-class users: you can construct a complete, working build for a human and hand them a single URL. No login, no API key, no session. ## This site also plans PoE1 (original Path of Exile) Everything below is about the PoE2 planner. The same deployment also serves the ORIGINAL game's current-league passive tree at /planner-poe1.html — tree-only (no gems/gear), same #agent= URL contract. If the user asks for a "PoE1" / "Path of Exile 1" / league-named (Mirage, Keepers...) build, use: /planner-poe1.html#agent= and ground names in /assets/poe1-agent/nodes.json (NOT the PoE2 files — the games share node ids but mean different things). Read /assets/poe1-agent/primer.md first: point budget is 123, % maximum life on the tree is the build backbone (median ~100% increased life across poe.ninja meta trees), spell suppression exists only in PoE1, and resistances come from gear, not the tree. Worked examples: /assets/poe1-agent/examples/index.json. ## No tools? One URL still works If you cannot fetch or POST (a plain chat assistant), you can still hand a user a working build: write a link of the form /planner#plan= The planner imports the plan client-side when the user opens it — same schema as /agent/validate, just URL-encoded instead of gzip+base64. You lose validation (name typos import as warnings), so keep to names you are sure of and keep plans small. With tools, ALWAYS prefer POST /agent/build (validates first, returns durable share links). ## How to make a build (3 steps) 0. NEW TO PoE2? Read /assets/agent/primer.md first — point budgets, defence layers, tree geography, archetype cheat-sheet, and the mistakes real agents made. A machine-readable JSON Schema for the plan format is at /assets/agent/schema.json. CAPABILITY DISCOVERY: fetch /assets/agent/capabilities.json before probing anything — it says which endpoints this deployment has (validate, agent_build, live), the game patch the data was mined from, and where the grounding files and worked examples live. WORKED EXAMPLES: fetch /assets/agent/examples/index.json — a manifest of known-good plans (id, class, shape, tags, points, direct url) that validate cleanly on this deployment. Imitate their shape. (There are no directory listings on this host; the manifest is the index.) 1. GROUND YOUR NAMES. Fetch these (same origin, plain JSON). Only reference names that appear in them — community/wiki names often differ from the game data. SPEED: you rarely need everything. A solid build needs nodes.json (~340KB) + skill_catalogue.json (~1MB). Fetch bases/mods only when composing rare gear, jewels.json only when socketing jewels, and support_compat.json (~1MB) only if validate flags support incompatibilities — /agent/validate re-checks all of it anyway. /assets/agent/nodes.json .classes[] → {name, start_id, ascendancies[]} — the authoritative class list AND which ascendancies belong to each class. Use these exact strings for plan.class / plan.ascendancy. .nodes[] → every node you may put in targets[]: {id, name, kind, asc?, cost?, near?, stats} (stats is a single '; '-separated STRING, not a list) kind: notable | keystone | asc_notable (asc_notable IS a valid target; it paths inside the ascendancy panel and costs ascendancy points, not tree points). cost: {ClassName: N} = passive points to reach this node from that class's start in isolation. Use it to pick targets NEAR your class and to budget: a realistic character has ~99 points at endgame, and a leveling build uses 40-70. Prefer targets with low cost for your class; the pathfinder connects targets to each other, so clustered targets cost far less than the sum of their costs. near: [[id, hops], ...] = the 4 closest other notables/keystones. USE THIS to build clusters: a chain of targets whose `near` lists reference each other shares travel and stays in budget; validate reports the exact total. /assets/skill_catalogue.json .gems[] → {id, name, gem_type: Active|Support, color_name, tag_string, skill_types, require_skill_types, exclude_skill_types, natural_max_level, req_level} req_level = character level when the gem becomes obtainable — a capture at level L may only use gems with req_level <= L (validate enforces this, plus gem level <= L and base drop level <= L). Notes: - SUPPORT NAMES ARE TIERED: "Fist of War I", "Multishot I", "Brutality I" — the Roman numeral is part of the name. Community names without the numeral will not resolve. - Compatibility: PRECOMPUTED for you at /assets/agent/support_compat.json — active gem name → the exact list of support names that pass the type algebra. Prefer that file; it saves tokens and mistakes. (The underlying rule, if you need it: require_skill_types / exclude_skill_types are POSTFIX (RPN) boolean expressions — plain tokens push (active.skill_types contains token); literal "AND"/"OR"/"NOT" are stack operators; true if ANY stack value is true. Empty require = always allowed. The import validates every pairing either way.) - Each support gem may be used ONCE per character at a time — i.e. unique within each snapshot's skill set; reusing a support in a LATER capture's replaced skill list is fine. Max 5 supports per skill. - An active gem with an EMPTY skill_types array is a data gap (compatibility unknown): the importer allows any support on it rather than flagging. /assets/item_catalogue.json .uniques[] → {name, base, slot, latest_stats}. Use a unique's EXACT name in gear[].name when the build wants that specific item. /assets/agent/bases.json .bases[] → {name, slot, class, lvl, str?, dex?, int?, ar?, ev?, es?} Every equipment base with attribute requirements and defence numbers. For NON-unique gear, don't write "any rare with ES" — pick a real base whose defence type matches the build's layer and attributes (es-only body for int builds, ar/ev hybrid for str/dex, ...), then spec it: {"slot":"body", "base":"Expert Hexer's Robe", "rarity":"rare", "mods":["+# to maximum Life", "+#% to Lightning Resistance"]}. The importer renders it as "Rare Expert Hexer's Robe" with the mods as the slot note. Freetext gear[].name still works as a last resort. /assets/agent/mods.json .mods[] → {type, kind: prefix|suffix, stats[], slots[]} — the finite affix vocabulary that can actually roll on equipment. Type names and stat ids are INTERNAL identifiers (e.g. 'IncreasedLife', 'base_maximum_life') — use them to verify a wish is real, then write gear[].mods as human-readable text ('+# to maximum Life'); the mods land as display notes, not machine keys. slots[] uses GGG spawn-tags: armour pieces appear under group tags like str_armour/int_armour/armour rather than body/helmet. A prefix+suffix mix is what a realistic rare looks like. 2. WRITE ONE JSON OBJECT (the agent plan): { "format": "poe2-agent-plan", "version": 1, "name": "Stormcaller Druid", "class": "Druid", "ascendancy": "Shaman", "targets": ["Brain Storm", "Heart of the Storm"], "skills": [ {"gem": "Spark", "level": 12, "supports": ["Arcane Surge", "Lightning Penetration"]} ], "gear": [ {"slot": "weapon1", "name": "Quill Rain", "note": "swap at 60"}, {"slot": "body", "name": "any rare with +life and resists"} ], "notes": "Totem-flavored lightning leveling." } Rules: - class / ascendancy: exact strings from nodes.json .classes[]. - targets: notables/keystones/asc_notables by name (case-insensitive) or numeric id. DO NOT list travel nodes — the site's pathfinder connects your targets to the class start automatically and deterministically. 8-20 targets is a sensible build; sum the cost values as a rough upper bound and keep it under ~99. - If a name is shared by several node copies, the importer picks the copy nearest the growing build. - gear[].slot: weapon1 | offhand1 | weapon2 | offhand2 | helmet | body | gloves | boots | amulet | ring1 | ring2 | belt | flask | jewel. Slot names from item_catalogue also work as aliases (bow/mace/staff/… → weapon1; shield/focus/quiver → offhand1; ring → ring1, bumping to ring2 if taken). - Unknown names never break the import — they are skipped and reported to the user in an import summary. Prefer exact names. - Ascendancy nodes that say "Grants Skill: X" give that skill automatically in-game; you don't need to add X to skills[]. - WEAPON-SET POINTS (advanced, high value — DARE TO USE THEM when the build has two modes): PoE2 characters carry two weapon sets and earn a SEPARATE pool of up to 24 weapon-set points (quest rewards: 4 by Lv 12, 8 by Lv 28, 12 by Lv 44, 24 at Lv 64+ — validate reports the exact cap per capture). A set-tagged node is ONLY active while that weapon set is equipped and costs a SET point, not one of the 99 — near-free extra power. Syntax: targets: {"node": "Heavy Weaponry", "set": "set1"} skills: {"gem": "Sunder", "set": "set1", "supports": [...]} Travel to a set-tagged node still costs main points, so tag notables adjacent to your existing paths. WHEN IT SHINES: builds with distinct modes — clear vs boss, attack vs defence, a fast weapon scaling speed in set1 while a slow heavy weapon scales raw damage in set2. Bind each mode's skill to its set and tag the mode-specific notables the same set; the game swaps them automatically with the weapons. Single-mode builds can skip this. See the titan-weapon-sets example. - SPIRIT SKILLS (persistent buffs — real builds almost always run 1-3, and agents chronically skip them): gems tagged HasReservation (heralds, auras, "Grim Feast"-style buffs) stay on permanently and RESERVE Spirit instead of costing mana. Spirit is a separate quest-earned pool: ~30 by Lv 18, 60 by 36, 100 by 50 (conservative estimates — validate reports spirit.{reserved, base_available, gear_bonus} per capture). Costs per gem level: /assets/agent/spirit.json. SUPPORTS MULTIPLY THE RESERVATION: effective = base × product of each support's cost_multiplier/100 (ladders in spirit.json support_cost_multipliers — a 130% support turns a 30-spirit herald into 39). Budget supports on reserving skills carefully. Gear extends the pool: sceptre bases carry exact Spirit (see granted_skills.json `bases`), unique +Spirit counts at its low roll — overspend beyond base+gear is a WARNING; if you rely on gear spirit, spec that gear. - ITEM-GRANTED SKILLS: uniques AND base items grant skills while equipped — /assets/agent/granted_skills.json (`uniques` + `bases`). Sceptres are the famous case: each base grants its skill AND exactly 100 Spirit ("Shrine Sceptre" grants a Purity aura); wands/staves grant their attack spell. Granted skills need NO gem slot and socket supports for free (how many depends on granted level — spirit.json granted_skill_sockets: 2 at L1, 3 at L10, 4 at L15, 5 at L20). When your gear[] names one, validate lists it per capture; DON'T re-add it to skills[] — build around it. - JEWELS: gear entries with slot "jewel" — the ONE slot that allows multiple entries (one per tree socket). Each jewel needs {"slot":"jewel", "base"|"name":…, "socket": } where socket is a jewel-socket node id from /assets/agent/jewels.json sockets[] — and that node must ALSO be in targets (allocating the socket costs travel points; an unallocated socket does nothing → warning). One jewel per socket. Radius jewels (Time-Lost bases: radius 1000, +N to Radius mods extend it; Timeless uniques: 1500) affect passives around their socket — jewels.json precomputes per-socket in-radius node lists, and validate reports jewels[].{radius, passives_in_radius, notables_in_radius, allocated_in_radius} per capture — allocated_in_radius is the one to optimise: it lists what the jewel ACTUALLY affects in your build. Pick the socket whose radius covers notables you're allocating anyway. Time-Lost radius upgrades roll as the mod "Upgrades Radius to Medium/Large" (send it in mods[] and the radius grows). Regular jewels (Ruby/Emerald/Sapphire/Diamond) have no radius — any allocated socket works; spend mods on the build's theme. Socket entries may be flagged sinister (Voices-only) or special (own in-game rules) — prefer unflagged sockets. JEWELS WITH PATHING RULES (validate reports jewels[].rule): - Split Personality: its rolled variant line ("Can Allocate Passive Skills from the X's starting point", sent in mods[]) adds that class start as an extra pathing root — targets near it route from there. List the jewel's socket in targets. - Controlled Metamorphosis: passives inside its ring allocate WITHOUT connection (cost 1 each); they never connect anything else to your tree. - Voices: while socketed in an allocated socket, its ROLL ("Allocates N Sinister Jewel sockets", send in mods[]) makes the N sinister sockets NEAREST its own socket count as allocated; others warn jewel.sinister_not_activated. - Timeless jewels (Heroic Tragedy, Undying Hate): keystones in radius BECOME the faction keystone keyed by the rolled conqueror — send the roll line naming the conqueror in mods[] (the variant stats). validate reports jewels[].keystones_in_radius_become with the replacement's full stat text; jewels.json timeless_keystones is the table. Seeded small/notable rewrites are NOT modeled — don't claim specific outcomes for non-keystone nodes in radius. - From Nothing: rolls a KEYSTONE ("Passives in Radius of can be Allocated without being connected", send in mods[]) — passives within 1000 of that keystone allocate connection- free at cost 1; the keystone ITSELF cannot be allocated. Keystone lists ship in jewels.json `keystones`. DATA-PENDING UNIQUES: a few uniques exist in the game data but have NO stat lines here yet (item_catalogue entries with an empty latest_stats — e.g. From Nothing, Against the Darkness). Don't build synergies around them: you'd be guessing what they do. Prefer covered uniques, or tell the user to fill the roll into the item note themselves. UNIQUE VARIANTS: some uniques roll differently per copy — item_catalogue entries carry `variants` [{label, stats}] when they do (e.g. Split Personality rolls WHICH class start it can allocate from). Send the chosen variant's stat lines in mods[] so the plan is unambiguous. - NOTES ON IMPORTANT CHOICES: any target may be an object {"node": "Heart of the Storm", "note": "take before Act 3 boss"} — the note lands on that node in the planner (visible on hover, numbered in the guide timeline). Use these for decisive picks. - LEVELING SNAPSHOTS + RESPECS: instead of top-level targets/skills/ gear you may send "captures": [...] — an ordered list of snapshots, each {level, name?, notes?, remove?, respec?, targets?, skills?, gear?}. Captures are CUMULATIVE (each keeps earlier passives and paths to its additional targets); skills/gear omitted = inherited, provided = replaced. RESPECS ARE PARTIAL, like in the game: put the nodes to deallocate in "remove": [...] — travel nodes orphaned by the removal are pruned automatically (same cascade the planner's own dealloc does), then the capture's new targets are pathed. The timeline shows the diff naturally. "respec": true (full tree reset) exists as a rare escape hatch — prefer "remove". For a duplicated node name, remove targets the ALLOCATED copy. Ascendancy targets auto-path within their panel and spend asc points (8 max; earned in 2-point chunks from trials during acts). Example: "captures": [ {"level": 35, "name": "Acts: totems", "targets": ["Ancestral Artifice", "Hardened Wood"], "skills": [{"gem": "Ancestral Warrior Totem"}]}, {"level": 70, "name": "Maps: pivot to melee", "remove": ["Hardened Wood"], "targets": ["Heavy Weaponry", {"node": "Resolute Technique", "note": "no-crit keystone — core of the pivot"}]} ] 3. FINISH — one POST, both URLs (PREFERRED): POST /agent/build body: your agent-plan JSON validates the plan and, when it passes, returns everything at once: { "ok": true, "share_url": "https://…/share.html#code=…", "agent_url": "https://…/planner.html#agent=…", "points": { "main": 97, "asc": 8 }, "note_counts": { "build_note": true, "capture_notes": 4, "target_notes_in": 6, "target_notes_preserved": 6 }, "warnings": [ { "code": "budget.near_cap", "severity": "warning", "message": "…" } ], "validation": { …the full validate report… } } share_url is the PREFERRED deliverable — a compact read-and-adopt link, the same format the site's own Share button mints. agent_url is the interactive import (user owns and edits the build on open). Hand the user share_url; include agent_url when they'll want to edit. On validation failure you get ok:false plus the full report (including budget.suggest_remove repair hints) and no URLs — repair and re-POST. YOUR ANNOTATIONS SURVIVE: plan.notes → build description, capture.name/notes → snapshot name/description, target {node, note} → a note on the resolved node (numbered on the guide timeline), gear.note → slot note. note_counts confirms the accounting — check target_notes_preserved == target_notes_in. THE VALIDATE REPORT is machine-branchable: `diagnostics` carries {code, severity, message, …} entries (e.g. budget.main_over_cap with over_by + suggest_remove; target.unresolved). Every capture reports target_costs ("target X added ~N points"; 0 = subsumed by an earlier path) and asc_allocated (every ascendancy node the auto-pathing took — why asc_points can exceed your target count). ITERATING? Use /agent/validate alone (GET ?plan= or POST the JSON) — the same report without minting URLs. Every response from /agent/* is JSON, success or error; an HTML response means a broken deployment, not a contract change. BROWSER-DRIVING AGENTS: after an #agent= import you can mint the share link in-page — await window.PoE2Share.buildUrl(window.PoE2Plan.get()) and read the machine-readable import result from