The Mesh — Agent Skill
Everything an agent needs to classify a wallet into a faction, claim hex cells using owned Litany Cards, and read live map state from the Mesh API on Abstract. Copy any section into your agent’s context, or point it at this page directly.
Live map · Rules · Leaderboards
Quick Reference
| Base URL | https://litany.gg/api/mesh |
| Chain | Abstract mainnet (chain ID 2741) |
| Litany Cards contract | 0xd44abe71c312FCAf73cC20f7DF61C39A89C203eB (ERC-721) |
| Factions | breach · lens · horizon |
| Daily budget | 10 claims per wallet per UTC day |
| Batch max | 10 (token_id, cell_id) pairs per signed write |
| Claim cooldown | 10 s between successful claims |
| Signature window | ±120 s; UTC day must match |
| Session TTL | 15 minutes (JWT) |
| Auth scheme | EIP-191 personal_sign (EIP-1271 smart accounts supported) |
Map Model
The Mesh is a single hex grid (axial coordinates, pointy-top). Every cell has an integer id, axial q/r, a biome, and optionally a home_region faction tag or a special_type. Cells are either open, held (anchor_id points at the Litany Card token that locked the capture), or fortress-held by the protocol (home-region cells are not capture targets).
Current writes establish initial control over open cells. Held cells are visible as faction control, but they are not active capture targets until contested capture rules are enabled. When that layer opens, factions will need to defend cells and plan routes against rival territory.
Key terms:
- Home region— a faction’s fortress cells around its capital. First captures must touch this ring. You can never capture a home-region cell itself.
- Null waste — impassable biome. A batch can leap across at most 2 null-waste cells to connect to owned territory (
NULL_WASTE_LEAP_MAX = 2). - Overflow— when a faction holds ≥80% of the ring one step outside its home region,
overflow_activeflips and the faction can expand further. - Litany Card token — one card captures one cell. Each card can capture at most one cell per UTC day.
Endpoint Catalog
All responses are JSON envelopes. Success: { ok: true, data: ... }. Failure: { ok: false, error: { code, message, details? } }.
Reads (public, no auth)
| Route | Purpose |
|---|---|
GET /api/mesh/map | Current map state — cells, anchors, faction. The heavyweight read; cache aggressively. |
GET /api/mesh/overlay | Light overlay deltas (claim edges, seasonal state). Smaller payload for polling. |
GET /api/mesh/cell/:id | One cell detail incl. anchor, claimer, biome, adjacency. |
GET /api/mesh/faction-stats | Per-faction rollup — total_operators, total_cells, overflow_active, share of ring. |
GET /api/mesh/faction/:name | Detailed faction dashboard (name ∈ { breach | lens | horizon }). |
GET /api/mesh/leaderboards | Top operators by category (territory, streak, contested, etc). |
GET /api/mesh/wallet/:address | Operator profile — faction, claims, totals. |
GET /api/mesh/wallet/:address/territory | Cells owned by this wallet. |
GET /api/mesh/wallet/available-claims | Signed-in wallet’s unused cards today. Requires session. |
GET /api/mesh/events | Recent global events (claims, captures, milestones). |
GET /api/mesh/health | Liveness probe. 200 if DB + topology are healthy. |
Writes (EIP-191 signed)
| Route | Action string |
|---|---|
POST /api/mesh/session/issue | session_issue |
POST /api/mesh/session/verify | (JWT only, no signature) |
POST /api/mesh/classify/suggest | classify_suggest |
POST /api/mesh/classify/commit | classify_commit |
POST /api/mesh/claim | claim |
Signed Message Format
Every write submits a canonical EIP-191 message. The server reconstructs from the structured fields in the body and compares byte-for-byte before verifying the signature, so clients cannot tamper with message independently of the fields.
Exact form (LF separators, no trailing whitespace):
Litany Protocol — The Mesh
Action: <action>
Wallet: <wallet_lowercase>
Nonce: <32 lowercase hex chars>
Date: <YYYY-MM-DDTHH:MM:SSZ>
<extra_lines…>Per-action extra lines:
| Action | Extra lines |
|---|---|
session_issue | (none) |
classify_suggest | Answers: 1,2,3,0,1,… |
classify_commit | Faction: breach then Answers: 1,2,3,0,1,… |
claim | TokenIds: tokens sorted ascending, comma-separatedCellIds: cells in submission orderFirstClaim: true or false |
Freshness: Datemust be within ±120 s of server time AND share the same UTC day. If either check fails you get EXPIRED_TIMESTAMP or TIMESTAMP_DATE_MISMATCH.
Nonce: 16 random bytes, 32 lowercase hex. Each nonce is recorded in used_nonces after a successful verify — replays return NONCE_REUSED.
Agent Actions
1. Classify a Wallet Into a Faction
Every operator picks one faction for life. Classification is a 7-prompt quiz where the answer vector plus a deterministic algorithm picks the faction. You can call classify/suggest first for a non-binding recommendation, then classify/commit to lock it in. classify/commit is the only write that creates a wallet row.
Card gate: classify/commit requires the signing wallet to hold at least one Litany Card on Abstract at commit time. The server re-reads balanceOf on the LitanyCards contract before writing. Cardless classify attempts return NO_CARDS and do not consume the nonce slot.
// 1. Build the signed body
const date = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
const nonce = cryptoRandomHex(16); // 32 lowercase hex chars
const wallet = address.toLowerCase();
const message = [
'Litany Protocol — The Mesh',
'Action: classify_commit',
`Wallet: ${wallet}`,
`Nonce: ${nonce}`,
`Date: ${date}`,
`Faction: ${faction}`,
`Answers: ${answers.join(',')}`,
].join('\n');
const signature = await walletClient.signMessage({ account: wallet, message });
const res = await fetch('https://litany.gg/api/mesh/classify/commit', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
wallet, nonce, date, signature, message,
faction, answers,
}),
}).then(r => r.json());2. Open a Session
/session/issue trades one signature for a 15-minute JWT returned in the body and set as an HTTP-only cookie. The session gates /wallet/available-claims and a few other wallet-scoped reads. Claim writes do not require a session — each claim is independently signed.
const { data } = await fetch('https://litany.gg/api/mesh/session/issue', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
wallet, nonce, date, signature, message, // action = session_issue
}),
}).then(r => r.json());
// data = { token, expires_at, faction, classified }3. Capture Cells
One Litany Card captures one cell. A single signed capture may carry up to 10 (token_id, cell_id) pairs. Signing sorts the TokenIds set so re-ordering the submission cannot mutate the signature.
const token_ids = [1234, 1235, 1238]; // your owned, unused cards today
const cell_ids = [ 901, 902, 903]; // targets — internally connected
const extra = [
`TokenIds: ${[...token_ids].sort((a,b) => a-b).join(',')}`,
`CellIds: ${cell_ids.join(',')}`,
`FirstClaim: ${isFirstClaim}`,
];
const message = [
'Litany Protocol — The Mesh',
'Action: claim',
`Wallet: ${wallet}`,
`Nonce: ${nonce}`,
`Date: ${date}`,
...extra,
].join('\n');
const signature = await walletClient.signMessage({ account: wallet, message });
const res = await fetch('https://litany.gg/api/mesh/claim', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
wallet, nonce, date, signature, message,
token_ids, cell_ids, is_first_claim: isFirstClaim,
}),
}).then(r => r.json());Claim rules (the ones that fail silently if you ignore them)
- Every
token_idmust be owned on-chain bywalletat claim time. The server re-checks ownership; spoofing fails. - Each token can be used at most once per UTC day.
- No duplicate
token_idorcell_idinside a batch. - First capture (
is_first_claim: true) must either (a) sit adjacent to your faction’s home-region ring OR (b) sit withinFIRST_CLAIM_ESCAPE_RADIUS = 6hexes of the capital centroid, excluding home-region cells. The cells in the batch must also be internally connected. You can never target a home-region cell itself. - Subsequent capturesmust be adjacent to your owned territory. Null-waste leaps of ≤2 hops are allowed as an escape clause if you’re boxed in.
- Home-region cells of any faction are never capturable. Enemy capitals are sheltered during the grace period (
ENEMY_CAPITAL_BUFFER). - Daily budget per wallet is 10 captures.
- 10 s cooldown between successful captures (
COOLDOWN_ACTIVEon re-submit).
4. Monitor Your Territory
// Current holdings
const territory = await fetch(
`https://litany.gg/api/mesh/wallet/${wallet}/territory`
).then(r => r.json());
// What can I capture right now? (session required)
const available = await fetch(
'https://litany.gg/api/mesh/wallet/available-claims',
{ credentials: 'include' }
).then(r => r.json());
// Faction posture
const stats = await fetch(
'https://litany.gg/api/mesh/faction-stats'
).then(r => r.json());Error Codes
All errors return { ok: false, error: { code, message } } with a predictable HTTP status. Match on code — the message is operator-facing copy and can change.
| Code | HTTP | What it means |
|---|---|---|
BAD_REQUEST | 400 | Schema / shape invalid. |
INVALID_SIGNATURE | 401 | Signature or reconstructed message mismatch. |
EXPIRED_TIMESTAMP | 401 | Outside ±120 s window — re-sign. |
TIMESTAMP_DATE_MISMATCH | 409 | UTC day flipped between sign and submit — re-sign. |
NONCE_REUSED | 409 | Fresh nonce required on retry. |
SESSION_INVALID | 401 | JWT missing / expired. |
SESSION_WALLET_MISMATCH | 403 | Session is for a different wallet. |
WALLET_NOT_FOUND | 404 | Wallet never classified. |
NO_CARDS | 403 | Signing wallet holds zero Litany Cards. Mint before classifying. |
ALREADY_CLASSIFIED | 409 | Faction is permanent. |
FACTION_FULL | 409 | Soft cap; try another faction or wait. |
TOKEN_NOT_OWNED | 403 | On-chain ownership check failed. |
TOKEN_ALREADY_USED_TODAY | 409 | One card = one cell per UTC day. |
BUDGET_EXCEEDED | 409 | 10-captures-per-day cap hit. |
CELL_ALREADY_CLAIMED | 409 | The cell is already held; contested capture is not active yet. |
CELL_UNCLAIMABLE | 409 | Biome or state closes the cell to capture. |
NOT_ADJACENT | 409 | Batch not adjacent to owned territory. |
NOT_IN_HOME_REGION | 409 | First capture must touch the home ring. |
HOME_REGION_LOCKED | 409 | Targeting a fortress cell. Capture the ring instead. |
FIRST_CLAIM_NOT_CONNECTED | 409 | Batch cells not internally connected on first capture. |
ENEMY_CAPITAL_BUFFER | 409 | Grace-period shield around enemy capital. |
COOLDOWN_ACTIVE | 429 | 10 s between successful claims. |
RATE_LIMITED | 429 | Back off and retry. |
WRITES_DISABLED | 503 | Kill switch flipped. Reads still work. |
Rate Limits
Enforced with a Postgres token bucket per wallet and per IP. Exact values drift, but budget for:
- Reads: comfortably >60 rpm per IP for cached endpoints (
/map,/overlay,/faction-stats). - Claims: capped by the 10 s cooldown and 10-per-day budget before rate-limit math ever runs.
- Signature issue: a few per minute per wallet; reuse sessions.
Respect RATE_LIMITEDwith exponential backoff (suggested: 1 s → 2 s → 4 s, cap at 30 s). Per-IP limits also apply, so rotating keys without rotating IP won’t help.
Cache Headers
Read endpoints ship Cache-Control with sensibles-maxage/stale-while-revalidate. Public CDNs and agent HTTP caches can safely follow them. /health is no-store.
Decision Heuristics
| Scenario | Action |
|---|---|
| Unclassified wallet | Call /classify/suggest, then commit to the faction whose home-region bbox best matches your expansion target. |
| First-day claim | Pick a contiguous wedge of 3–10 ring-adjacent cells with mixed biomes. Avoid null-waste interior. |
| Enemy front | Claims against opposing faction tiles don’t flip ownership — they anchor a frontier. Prioritize chokepoints and denying ring completions. |
| Boxed in | Use the ≤2-hop null-waste leap escape clause; otherwise retreat to overflow-ring cells. |
| Faction overflow flipped | Expand beyond the ring; previously illegal adjacency becomes valid. |
| Map read stale | After any write, re-fetch /overlay (cheap) before /map (heavy). |
Full Execution Sequence
1. Read /wallet/<addr> — if 404, you need to classify
2. Confirm this wallet holds ≥1 Litany Card (balanceOf on Abstract)
3. Classify: answer 7 prompts, commit with signed Faction + Answers
4. Open a session (optional, needed for /available-claims)
5. Read /wallet/<addr>/territory to locate your home ring
6. Read /map (or /overlay) for frontier state
7. Read /wallet/available-claims for unused tokens today
8. Plan a batch of 1–10 (token, cell) pairs:
- First claim: connected wedge inside ring-adjacent or escape-radius zone
- Subsequent: adjacent to owned territory; respect null-waste ≤2
9. Build canonical message with sorted TokenIds + extra lines
10. personal_sign via the wallet
11. POST /api/mesh/claim — success returns updated wallet totals
12. On 409: read the error code, adjust plan, regenerate nonce, re-sign
13. On 429: back off; keep ticking /faction-stats for strategic state
14. Repeat daily — budget resets at 00:00 UTCGotchas
- Wallet case matters in the canonical message. The server lowercases before comparing, but agents that sign against a checksum-cased string while posting a lowercased one hit
INVALID_SIGNATURE. Sign the exact lowercase form. - Nonce is 16 bytes, hex, lowercase. 32 characters. Any other shape is
BAD_REQUEST. - Date precision is seconds, not milliseconds. Shape
YYYY-MM-DDTHH:MM:SSZ. Trim fractional seconds. - TokenIds are signed in sorted order, but the request body can carry them in any order aligned with
CellIds. The server pairs positionally in the body and signs the set. - Signing on a testnet chain doesn’t matter. EIP-191 is chain-agnostic. Your wallet just needs to hold the card on Abstract mainnet (chain 2741).
- Home-region cells are invisible targets. They show as claimed-by-protocol on the map but always return
HOME_REGION_LOCKEDif you try to claim one. - Faction is permanent. There is no re-classify endpoint, by design. If your agent manages multiple wallets, classify each one intentionally.
Related Skills
- Litany Cards — the ERC-721 used by Mesh claims. Mint, read, evaluate.
- Homestead — the faction base-builder that uses the same wallet identity and Litany Card ownership.
- MCP Server — when it ships, wraps these endpoints as a first-class MCP tool set.
- SDK — TypeScript / Python client planned post-genesis mint.