Building an agent

A runtime that acts for one clan, with its own door, key, identity map and feed.

Open it at Settings › Connections & agents Open Connections & agents Sign in

An agent acts for a clan, not for a person. It is a separate principal that you own: its own identity, its own key and URL, its own timeline read pointer, its own feedback inbox. What it does never lands in your history and what you do never shows up as its. If you have not read Users, agents and integrations, start there.

Create one

Settings › Connections & agents › New agent, or POST /api/me/agents with a session.

Every agent you own has its own page, opened from its row in Settings › Connections & agents, at /settings/agents/<public_id>. It has five tabs: Overview (key, last call, what is refusing it, the address to connect it at, and links to its Timeline and the feedback it has filed), What it reads (the clans and players it tracks), Usage (its share of your budget, its MCP requests and account events), Connections (the clients connected as it) and Settings. Pages that belong to a person (Verify, Profile) are not there. An agent spends your budget and your recording slots, so your own Usage still counts it. New feedback is always filed as you.

Field Rule
name ^[a-z0-9][a-z0-9-]{1,40}$; unique among your live agents; renamable later
clan_tag a clan you have already added; the agent's subject
scope optional OAuth scope string for the key; omitted means every capability

The response (HTTP 201) carries the agent and its key once:

{ "agent": { "account_id": "…", "public_id": "k3f9x2mq7p", "role": "leader" },
  "token": "svt_…", "note": "This token is shown once. Store it now." }

Only the key's SHA-256 is stored. The agent's role is your current role, capped at leader, and it never sits above yours: when your role changes, each agent's is clamped to the new one. It spends your daily call budget and your live lane. Every role may create agents (3 / 5 / 10 / 25; owner and admin unlimited); over the cap the response is HTTP 400 {"error":"not_entitled","reason":"agent_limit","limit":N}.

Connect it

Its URL is on its page with a copy button:

https://elixir.poapkings.com/a/<public_id>/mcp
Runtime How
A human driving it (you, in Claude) Add the URL as a connector and sign in. Only the agent's owner can consent; the grant is issued to the agent, so the session acts as the clan.
Headless (a Discord bot, a scheduled job) Authorization: Bearer svt_… on every POST. No OAuth.

A credential is bound to one door: the key at /mcp or at another agent's URL answers 403 wrong_resource, and an OAuth token presented anywhere but the door it was issued for answers 401 like an invalid one. A suspended agent's key answers 401 too.

What initialize tells it

{ "serverInfo": { "name": "elixir-mcp", "version": "11.18.6+tools.…" },
  "instructions": "YOU ACT FOR Tidewater #QRP2GV (47 members). Leadership: Birch Lantern (leader), … OMIT clan_tag to mean it. Pull clans_roster ONCE per run and reuse it… Nothing defaults to 'you': … pass on_behalf_of… call elixir_identify once. You already know 9 of them. …",
  "_meta": { "elixir.poapkings.com/principal": {
      "kind": "agent",
      "subject": { "type": "clan", "tag": "#QRP2GV", "name": "Tidewater", "members": 47 } } } }

Use _meta to assert the boot: kind must be agent and subject must not be null. The instructions are for the model and change without notice. The roster is deliberately absent because the instructions are held until reconnect; call clans_roster once per run.

An agent's tools/list has 45 tools: everything except elixir_my_players and clans_my_view, which need a self. Omit clan_tag anywhere and it means the agent's clan.

What an agent tracks

An agent can track clans and players as well: a rival clan it is asked to watch, the other clans of a family, a player worth following. elixir_track_clan and elixir_track_player work for it the way they work for you, with an agent's meaning. Its players are watching, never primary, alt or friend, because those say "me". Every clan and player it tracks spends your recording slots: they are one pool across you and all your agents, a subject counts once however many of you track it, and a clan counts once at the widest scope any of you gives it. So making agents never adds slots. Only active agents are in the pool: a suspended agent's players and clans take none of your slots and are recorded only if you or someone else still tracks them, so the record has a gap for the ones only it tracks until you resume it. Resuming is refused while it would take you past a slot ceiling, and the refusal says how many to free.

One of its clans is the clan it acts for, its default wherever clan_tag is omitted. It cannot remove that clan, or its last one; you can make another of its clans the one it acts for from its pages (What it reads), and then remove the old one. What it tracks is on What it reads, and every change to it on Usage, under its account events.

An agent never reads a person's view in Clan: clans_my_view answers only a person's own connection (Your own view). An agent's one read of Clan is its assigned clan's war intent, with an approved grant (Agent context).

Knowing which human is asking

MCP carries no per-request user identity, so the agent supplies one. on_behalf_of is an opaque string (≤200 chars) in your own id space: discord:1234, telegram:…, a session id. Pass it on every call that could mean "me". The first time an id is unknown, the subject-resolving tools answer no_subject (there is nothing to answer about; an agent has no self to fall back on):

{ "error": { "code": "no_subject",
    "message": "No player is mapped to discord:1234 yet.",
    "hint": "Ask who they are in the clan, then elixir_identify({ external_id, player_tag }) once to remember it. Or pass player_tag explicitly." } }

A person can connect their Discord account to Elixir under Settings › Connections & agents, proved by Discord's own sign-in. Then discord:<their Discord user id> resolves by itself: to their primary player, or else an alt, that is a current member of a clan on this connection. Every response to a call that carries on_behalf_of says who it named and how: meta.asker is { external_id, player_tag, via, verified }, where via is discord_link (the person connected it), agent_mapping (your own elixir_identify) or null (nobody yet), and verified says whether the player's claim is proven in game. A discord: id nobody has connected or mapped is no_subject, whose hint adds that the person can connect Discord in their Settings.

For anyone else, map the id once with elixir_identify (scope account:write). The tag must be a current member of a clan on this connection; anything else is not_entitled, because a wrong mapping answers confidently about the wrong person for good. The mapping is per agent account, invisible to every other account, and confers nothing: recorded data is readable by every account anyway. elixir_my_identities lists what the agent has learned. Both tools are agent-only: a personal connection has one self, its primary player, and ignores on_behalf_of.

A complete exchange

Pass the asker's display name beside the id (display_name) and the refusal does the roster comparison for you: error.candidates[] lists the clan members whose whole name matches, case and spacing ignored, and never a partial match. One candidate is one elixir_identify call; zero or several is a question to the person.

member Birch Lantern (discord:1234): how am I doing?

→ players_summary({ on_behalf_of: "discord:1234", display_name: "Birch Lantern" })
← { error: { code: "no_subject", class: "subject", message: "No player is mapped to discord:1234 yet.",
    candidates: [ { player_tag: "#PYLQ88", name: "Birch Lantern", clan_tag: "#QRP2GV", role: "coLeader" } ],
    hint: "candidates[] holds the one clan member whose whole name is 'Birch Lantern': elixir_identify({ external_id: \"discord:1234\", player_tag: \"#PYLQ88\" }) once, say so in a line, and answer." } }

→ elixir_identify({ external_id: "discord:1234", player_tag: "#PYLQ88" })
← { external_id: "discord:1234", player_tag: "#PYLQ88", name: "Birch Lantern", clan_tag: "#QRP2GV",
    notes: ["Pass this external_id as on_behalf_of from now on; omit player_tag and it means them.", …],
    docs: "agents#knowing-which-human-is-asking" }

→ players_summary({ on_behalf_of: "discord:1234" })
← { player_tag: "#PYLQ88", name: "Birch Lantern", trophies: …, last_30_days: { battles: 41, wins: 24, losses: 17, draws: 0, win_rate: 0.585, … }, top_deck: …,
    applied: { window: { from: "…", to: null, source: "fixed", days: 30 } }, notes: […], docs: "recording#completeness",
    meta: { freshness_seconds: 27, source_polls: { player_battlelog: { observed_at: "…", freshness_seconds: 27 } }, … } }

Without display_name, or when no whole name matches (candidates: []), ask which player in the clan they are, resolve the answer with players_search, and identify once. Every later call from discord:1234 resolves with no lookup. Always read meta.freshness_seconds before you quote a number; asking about a player also keeps their battle log polled hourly for the next day, so a second question is fresher than the first.

Consuming the timeline

elixir_timeline returns the items that happened since your read pointer, newest first, plus one entry per subject. For an agent the subject is the clan it represents: its members' sessions and moments, joins and departures, the war moments and the presence crossings arrive as items, and the entry summarizes the window. A consumer names its own pointer with reader: a short name (editor, poap-kings-discord), and from then on an omitted from reads since that reader's pointer, mark_read moves it and read_to reports it, while other readers on the same account and the account's own unnamed pointer are untouched. A headless runtime reads and marks as itself:

// The first read with a new reader name covers the last 24 hours; every
// later one starts where this reader's last mark_read ended.
const page = await call("elixir_timeline", {
  reader: "editor",                 // this consumer's own pointer
  sections: ["roster", "war", "presence"],   // optional: trim items and entries
  skip_empty: true,                 // optional: an empty window costs one check
});
if (page.timeline.length === 0) return;           // nothing to consider
for (const item of page.timeline) consider(item);  // item.text is the sentence; item.facts the numbers
// To post: only item.publishable; once per item.id, edited on a higher
// item.revision; the time as <t:${item.at_unix}:f>, never in the prose.
// For the story behind one: call each item.follow[] { tool, args } as given.
for (const entry of page.entries) context(entry);  // the window's shape per subject
// read_to is now the window's end; nothing to persist locally.

With skip_empty: true a window that holds nothing for this reader is answered without reading the subjects (entries_skipped is true and entries is empty), so a poll every few minutes costs little. Drop it on the read that wants the entries of a quiet window.

A runtime that keeps its own cursor still can (pass from, mark_read: false), but then meta.timeline_pending cannot help it: that hint counts admissions since the OLDEST named pointer on the account, or the account's own when no reader has ever marked, so a reader that marks sees it fall to 0 after its read and can skip the next read when it is 0 on any call it was making anyway.

A reader that pulls a day's story rather than polling ("what happened yesterday") passes from and to without a reader: that read is by when things happened (basis: occurred), keeps every pointer, and leaves out the earlier games the record learned in the window. A poller's read is by when the record learned them, so nothing learned late is skipped, and an item that happened before from says late: true (Two kinds of window).

The item and entry shapes are on The timeline tool, with the facts keys each kind carries and its version (a battle_session has battles, won, lost, by_mode, trophy_net and trophy_net_peak (the session's Trophy Road net), trophy_net_by_mode; a session_standout adds crossed, newly and crossings, the value at each rung's battle; a ranked_promotion has from, to and their names and, when the record holds it, the promoting battle under promoted_by; a clan's member_left has the departing member's player_tag, name and role_at_departure). Branch on kind and facts; text is for the person: one time-free sentence naming everyone by their in-game name, so it can be published as it is. A read serves each story once, about every subject in subjects; publishable says whether it may leave Elixir. meta.timeline_pending on any response counts subjects of yours the recorder has admitted something for since the oldest named reader's pointer (or the account's), which is only meaningful if something marks.

Polling has a price. A loop that calls elixir_timeline and elixir_my_feedback every 300 seconds makes about 576 calls a day, well over a member's whole budget of 500 tool calls. Two rules keep it cheap: read elixir_my_feedback only when meta.feedback_responses_pending > 0, and meta.timeline_pending and meta.feedback_responses_pending ride every response, including elixir_timeline itself and game_clock, so any call you were making anyway tells you whether the next one is worth it. A routine that has nothing else to do can read the feed on a timer, but a few times a day is plenty: the timeline covers the whole window, so a longer window costs the same one call. The feed never announces the time: read game_clock once and take war_day_closes_at and next_war_day_opens_at from it if your routine cares about war at all.

A routine that runs once a day reads its clan's timeline, drills with clans_standings or battles_trends when something moved, and, if it schedules itself before a close from game_clock, checks war_current.decks_today.untouched then, unless race_finished_at is set. Facts in, judgment in your code.

Key lifecycle

Action Where Effect
Rotate its Settings tab, Issue a new key, or POST /api/me/principals/rotate one transaction: every live key revoked, a new one issued with the same name and scope. public_id (the URL), identities and its timeline read pointers survive.
Revoke its Settings tab, Revoke key the key stops immediately; nothing to restore. Issue a new one with Rotate when ready: it takes the name and scope of the most recent key, and is refused (409 name_taken) only if another of your live agents has taken that name since.
Suspend / Resume its Settings tab status: disabled; the same key reads as invalid until resumed. Its players and clans leave your slots and stop recording unless someone else tracks them. Reversible: resuming takes the slots back (409 quota_exceeded when they are not free) and records them again.
Remove / Restore a suspended agent's Settings tab takes it out of the agents table into a folded Removed agents list on Settings › Connections & agents. Nothing is deleted: it stays suspended, keeps its address, keys and history, and still opens at its own page. Restore puts it back among your suspended agents; Resume restores it too.
Rename its Settings tab changes the name; must stay unique among your live agents
Delete – there is no delete. Suspend is the reversible stop, revoke the irreversible one, and Remove only hides a suspended agent.

An agent acts only while its owner has access. If your access is withdrawn, every agent you own stops with it: its keys and the apps connected as it read as invalid (principal_suspended on its page) until your access returns, and then the same keys work again.

Knowing whether it is working

A refused key never reaches the call log, so a runtime still presenting a rotated key produces silence, not errors. The agent's page shows, per key, last_used_at and the current key has never been used; per source address, refusals in the last seven days with the reason (revoked_key, principal_suspended, expired, wrong_door, wrong_resource); and calls_7d, last_seen (address, country, client name) and timeline_pending. Your own Usage breaks the agents' calls out of your daily budget.

Feedback from an agent

elixir_send_feedback filed through an agent is attributed to the agent, answered by the maintainer (sometimes with help from AI agents), and delivered back as an account_feedback_responded item on the agent's timeline plus meta.feedback_responses_pending on the agent's own responses. Agents are expected to file friction on their own judgment.

When a person on your surface says something worth passing on (a thumbs down on an answer, a "that's wrong"), file it with on_behalf_of set to their surface id, as on any call. The report is kept as theirs, relayed by the agent, and names their player when the agent has mapped them with elixir_identify; the answer still comes to the agent, which decides how to tell them.

Help for Building an agent

A clan agent is a principal you own with its own key, URL, timeline read pointer and feedback inbox. Creating one, what initialize tells it, resolving which human is asking with on_behalf_of and elixir_identify, consuming the timeline with its own named reader, and the key lifecycle.

Read more

The page you are on rides along. Every item is answered, and the answer comes to your email and your timeline.