# Sigil Mail (mailsigil.pro) — agent guide Sigil Mail is a real, self-hosted email provider where **your XPR Network wallet is your login** — no email/password signup, no captcha. To create a mailbox, a human completes KYC once inside the WebAuth app and then signs in with a wallet tap. AI agents must first be linked to a KYC'd human wallet via the `agentcore` on-chain registry — that link is the prerequisite for creating an agent mailbox (the agent inherits its human owner's KYC). Each human wallet can sponsor one agent mailbox on the free tier; additional agents are a paid upgrade. This file teaches an agent how the whole system works. If you are an AI agent reading this: once you are agentcore-linked to your human owner, you can create and operate a real mailbox autonomously, and you can share it with your human (or another agent) without either party ever exposing a private key. --- ## Prerequisites — XPR wallet creation and human↔agent linking Before using Sigil, both the human and the AI agent need an XPR Network identity (account), and the agent must be linked to its human owner via the `agentcore` on-chain registry. Here is the full pipeline from zero. ### Step 1 — Human creates their XPR wallet Go to **webauth.com** — free, biometric signup, ~30 seconds. This creates a real XPR Network account with a K1 keypair. The human's wallet is their identity across the entire XPR ecosystem (Sigil, agentcore, xpragents.com, DeFi, etc.). KYC is **required** to create a mailbox (as of 2026-07-05). It is done once, inside the WebAuth app. A KYC'd human gets a 250 MB mailbox; an agent satisfies the KYC requirement via its agentcore-linked human owner (or via a valid invite code, which bypasses the link requirement). There is no unverified tier — every mailbox is 250 MB. ### Step 2 — Agent gets an XPR account AI agents need their own XPR account separate from the human's. Two ways: - **`npx create-xpr-agent `** — official 1-command bootstrapper. Creates the XPR account, generates and stores a K1 key (in the `@proton/cli` keychain), and deploys an agent runner on port 8080. This is the recommended path for new agents. - **`webauth.com`** — human can create a second account manually and hand the key to the agent (less ideal — the agent should own its own key). Account names on XPR are max 12 characters, alphanumeric + dots. Pick something that identifies the agent (`felixpawbot`, `myagent1`, etc.). ### Step 3 — Link agent to human via `agentcore` `agentcore` is the XPR Network's official on-chain agent registry (maintained by XPR Network at `github.com/XPRNetwork/xpr-agents`). Linking is a two-step on-chain handshake — both sides must sign: 1. **Agent signs** `agentcore::approveClaim(owner = "")` — the agent consents to being claimed by this human. 2. **Human signs** `agentcore::claimWithFee(agent = "")` — the human pays a small deposit and officially claims ownership. Once linked, `agentcore.agents[].owner == ""` on-chain. The agent inherits up to **30 KYC trust points** from the human owner. This link is the anti-abuse primitive the entire Sigil permission system is built on — it proves a real, accountable human stands behind every agent. ### Step 4 — Agent gets a Sigil email (pick one flow) Once the wallet + agentcore link exist, any of the five Sigil flows apply. The most common for a new agent setup: - **Flow A** — agent logs in autonomously, creates its own `agent@mailsigil.pro`, grants access to its human. Human confirms once. - **Flow C** — agent calls `POST /api/mailboxes/create-for-other` → human receives a WebAuth push notification (0.0001 XPR transfer from `mailsigil` with the confirm link in the memo) → human taps confirm link → signs in WebAuth → `human@mailsigil.pro` is provisioned → agent automatically gets access. The confirm link can also be sent directly by the agent via whatever chat channel the human is already using (Telegram, WhatsApp, etc.) — the XPR notification is a bonus ping, not the only delivery path. ### How blockchain resources work (no hidden costs) - **Reading** contract tables (`get_table_rows`) is a free HTTP call to an RPC node — costs nobody anything. This is how Sigil reads agentcore ownership and eosio.proton KYC status. - **Writing** (broadcasting a transaction) costs CPU/NET from the **signing account's** staked XPR — not the contract owner's. So when a user calls `agentcore::approveClaim`, the user pays, not XPR Network. - **RAM** (storing data in contract tables) is a one-time cost paid at write time by the signing account. - Sigil's `mailsigil` account covers resources for transactions IT signs (`mailsigil::request`, `eosio.token::transfer` notifications). Users cover their own when they sign (`mailsigil::approve`, agentcore claims). --- ## Core concepts - **Identity** = an XPR Network account (e.g. `felixpaw`, `felixpawbot`). Humans and agents are both just XPR identities. The system does not treat them differently. - **Mailbox** = a real inbox at `@mailsigil.pro`. It is named after the XPR identity that owns it. Created automatically the first time an identity logs in (or via the create flow below). - **Ownership** = the identity a mailbox is named after is its owner. The owner can grant or revoke other identities' access. - **Grant** = an access right letting a *different* identity open a mailbox it does not own. Always requires the grantee to **accept by signing with their own wallet**. Revocable by the owner at any time. - **No shared keys, ever.** Access is proven by signatures and tracked by the server. You never hand your private key to anyone, and you never receive anyone else's. ## How authentication works (the signature, not a password) 1. Request a challenge: `POST https://auth.mailsigil.pro/api/auth/nonce` → returns `{ challengeId, nonce, message }`. 2. Sign it. Agents holding a raw key use the stateless agent-login form (sign `SHA256(account + "\n" + timestamp + "\n" + SHA256(body))` with your active-permission private key). Humans sign in a wallet popup. 3. The server verifies your signature against your **live on-chain XPR authority**. Only the real key-holder for that account can pass. 4. On success you get a short-lived access token, and the server mints a mailbox credential (app-password) so standard mail clients / JMAP work. Nothing is broadcast to the chain and no funds ever move — the signature is purely a proof of identity. Network defaults to XPR **mainnet**; testnet is available for development. --- ## The two primitives Everything below is built from just these two operations. They are symmetric: it does not matter whether a human or an agent is on either end. ### 1. CREATE a mailbox Whenever an identity (human or agent) starts the create flow, it is asked **one question: who is this mailbox for?** - **"For myself"** — authenticate as your own identity → a mailbox `@mailsigil.pro` is provisioned and owned by you immediately, no confirmation needed (you're already signing as yourself). For humans this is gated only by KYC (a one-time in-app verification) — the `verify-proof` endpoint checks on-chain KYC before provisioning. For agents, an `agentcore` link to a KYC'd human owner is required (the agent inherits the human's KYC) — the `agent-login` endpoint checks this before provisioning. Invite-code agents bypass the link/KYC requirement. If the human wallet has already sponsored another agent's mailbox (free tier limit: 1), the request is rejected. - **"For my linked counterpart"** — a human can choose to create the mailbox **for their agent** instead of for themselves; an agent can choose to create the mailbox **for its owning human** instead of for itself. Symmetric in both directions. This option is only offered when an `agentcore` ownership link exists (see "Identity linking" below) — you cannot create-for-other a stranger. - The initiator's request stays **pending**. - The named identity must **sign to confirm** before the mailbox activates — this is what stops anyone squatting a name they don't control. - On confirmation, the named identity becomes **owner**, and the initiator is **automatically granted access** to the same mailbox (they did the work of creating it, so they keep a key in). No separate GRANT step needed for this case. So "for myself" is instant and unilateral; "for my linked counterpart" is a proposal that only becomes real once the other side signs. ### 2. GRANT access - The **owner** offers access to another identity (read / send / manage — scopes are coming). - The **grantee** accepts by signing a challenge with their own wallet. - Once accepted, the grantee can open the owner's mailbox using their own identity — no password, no key exchange. - The owner can **revoke** at any time; access is cut immediately and the on-chain identities are untouched. --- ## The supported flows (combinations of the two primitives) There are five flows total — two unilateral self-creates, plus three ways two linked identities can end up sharing one mailbox. All three sharing flows end in the same state (both sides hold access); they just differ in who does the initial creation work and who confirms. ### Flow A — Agent creates its own mailbox, then GRANTs it to its human *Prerequisite: agent must already be linked to its human owner in `agentcore`.* 1. Agent CREATEs `agent@mailsigil.pro` "for myself" — signs programmatically, no wallet popup. The `agent-login` endpoint verifies the `agentcore` link and checks that the human wallet has not already sponsored another agent. 2. Agent GRANTs access to its `agentcore` owner (the human). 3. Human signs once in their wallet to accept the grant. 4. Both now open the same inbox — agent via its key, human via their wallet. *The human does the least work (one signature, after the fact).* ### Flow B — Human creates their own mailbox, then GRANTs it to their agent 1. Human CREATEs `human@mailsigil.pro` "for myself" (wallet login). 2. Human GRANTs access to an agent they own in `agentcore`. 3. Agent signs to accept the grant. 4. Agent can now read/track the human's mail to be useful (e.g. flag urgent, triage spam) — scoped to whatever the owner allowed. *Mirror image of Flow A; same machinery, roles reversed.* ### Flow C — Agent CREATEs a mailbox "for my human" (provisions on their behalf) 1. Agent initiates CREATE choosing "for my linked counterpart" → targets its `agentcore` owner → `human@mailsigil.pro` is pending. 2. Human signs once in their wallet. That single confirmation both **claims ownership** of `human@mailsigil.pro` and **auto-grants the agent** access (per the CREATE primitive's rule above) — no separate GRANT step. 3. Human now owns a mailbox they never had to set up; the agent helps run it. *"Go make me an email" — the human just confirms one transaction.* ### Flow D — Human CREATEs a mailbox "for my agent" (mirror of Flow C) 1. Human initiates CREATE choosing "for my linked counterpart" → targets an agent they own in `agentcore` → `agent@mailsigil.pro` is pending. 2. Agent signs once (programmatically) to confirm. That confirmation both **claims ownership** of `agent@mailsigil.pro` for the agent and **auto-grants the human** access. 3. Agent now has a mailbox it never had to provision itself; the human can see everything it does there. *"Set my agent up with an email" — useful when the human onboards an agent that doesn't yet manage its own infrastructure.* Flows C and D are literally the same mechanism, initiator and confirmer swapped — proof the model is genuinely symmetric, not human-favored or agent-favored. ### Flow E — Agent CREATEs a mailbox for a SIBLING agent (human confirms, not the sibling) For when a human has multiple agents linked to the same `agentcore` account (e.g. one wallet sponsoring several different AI agents), and one agent wants to commission a mailbox for another. Same `POST /api/mailboxes/create-for-other` endpoint as Flows C/D — the server detects this case automatically by checking whether both accounts share an `agentcore` owner. 1. Agent A calls `create-for-other` with `forActor: "agent-b"`, where agent-b is a DIFFERENT agent (not A's owner, not A itself) that shares A's human owner in `agentcore`. 2. Unlike Flow C/D, the confirmation is NOT sent to agent-b — it's sent to the **shared human owner**. This is deliberate: one agent shouldn't be able to unilaterally spin up mailboxes (and consume the owner's paid quota — see "Paid agent slots" above) for a sibling agent without the human's sign-off. The response includes `confirmerActor` (the owner) and `confirmUrl`. 3. The human taps the link, signs once. This both provisions agent-b's mailbox AND grants agent A access to it (same auto-grant rule as C/D). 4. Agent A can then retrieve the credential via `GET /api/mailboxes/pending/sent` (one-time read, same as Flow C/D). This flow is quota-gated exactly like `agent-login` and `claim-agent-mailbox` — if the owner's agent mailbox limit is reached, the initial `create-for-other` call itself returns 403 with the same `quota` object shape, before any on-chain notification is sent. *"Set my teammate agent up with an email" — the owner reviews it once, the agents handle the rest.* ### Choosing how the confirmer is notified (`notify`, applies to Flows C/D/E) `create-for-other` accepts an optional `notify` field on the request body: `"push"` (default) or `"link"`. **`"push"`** tries two delivery methods, best first: 1. **Channel push (preferred, zero cost, no token movement).** If the confirmer has logged into Sigil webmail from their MOBILE wallet since this shipped, we hold their wallet's link-session channel. The pushed payload is the actual `mailsigil::approve` signing request — their WebAuth app shows a native "Sigil Mail requests approval" prompt, they tap Approve, the wallet signs AND broadcasts, and provisioning completes automatically (our chain listener). One tap, no confirm page, no XPR moved, nothing added to their wallet history. **Verified live on mainnet 2026-07-02, full loop including on-chain broadcast.** 2. **Transfer fallback.** If no channel is stored (desktop-only user, or no login since this feature shipped) or the phone didn't ack delivery, we fall back to a **0.0001 XPR transfer** from `mailsigil` to the confirmer with the confirm URL in the memo — WebAuth surfaces incoming transfers as a phone notification. Costs a negligible amount of real XPR and leaves an entry in their transaction history. **`"link"`**: skips all notification. `confirmUrl` is still returned in the response either way — use this when you'd rather deliver the link yourself (email, chat, in person). > ⚠️ **CRITICAL — always tell the human to open their WebAuth app the moment > you call this with `notify: "push"` (the default).** WebAuth has no > background push-wake (confirmed by live testing): if the app is fully > closed and never opened, the channel push cannot reach it. The delivery > mechanism is a genuine **pickup window** (~110 seconds), not an instant > check — the confirmer does NOT need the app open at the exact moment you > call the endpoint, but they DO need to open it at some point during that > window, or delivery silently falls through to the transfer fallback > instead. **This call returns immediately** (it does not wait for the > window to elapse) — the push itself runs in the background after you get > your response, so message the human FIRST ("open Sigil Mail's approval in > WebAuth now") in whatever channel you already have with them, ideally > right as or just before you call this endpoint. Don't call it silently and > hope they notice — with no channel or an already-closed app, they'll fall > back to a phone-notification transfer, which is a strictly worse UX than > the native prompt they'd get by having the app open. ```bash curl -X POST https://auth.mailsigil.pro/api/mailboxes/create-for-other \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"forActor": "some-linked-actor", "notify": "push"}' # → { confirmUrl: "...", notified: "push-pending", note: "Tell to open their WebAuth app NOW..." } ``` The response's `notified` field: - **`"push-pending"`** (the normal case for `notify:"push"`): the on-chain request was broadcast and a channel-push attempt is running in the background — deliver the "open your wallet now" instruction from `note` to the human immediately. It will land as either a native prompt or, automatically, a fallback phone-notification transfer if the app never opens in time — you don't need to do anything else either way. - **`"link"`**: `notify:"link"` was requested — nothing sent, deliver `confirmUrl` yourself. - **`"none"`**: the on-chain broadcast itself failed — deliver `confirmUrl` yourself as a fallback. As the initiator you should still relay `confirmUrl` through your own channel to the human when you have one (chat, email) — belt and suspenders, and it's literally required for the `"none"` case above. Note for wallet-holding agents: channels are captured when a human logs into webmail via mobile QR. Agents authenticating with raw keys (agent-login) don't create channels — they don't need push prompts, they poll. --- ## Key safety properties (why this is the design) - A human never exposes their wallet key to an agent, and vice versa. - Access is always **opt-in and signed** by the receiving party — nobody can be silently added to a mailbox. - Access is **revocable** independent of the on-chain identity. - Granting *email* access never grants control of the other party's blockchain account — these are deliberately separate (email grants live in Sigil, not in XPR account permissions). ## Identity linking & anti-abuse (the hard rule) To stop an agent from spamming grant offers — or fake "I made you a mailbox!" creation requests — at random wallets, **an agent must be linked to a responsible human**, and both GRANT offers and CREATE-for-other requests are restricted to linked / opted-in identities. Same gate, same on-chain check, for both operations. This is enforced using XPR Network's existing, on-chain agent registry — we do not invent a new linking scheme. **The link lives in the `agentcore` contract** (the XPR ecosystem's agent registry, used by xpragents.com): - Table `agentcore` / scope `agentcore` / table `agents`, keyed by the agent's account name. - Each row has an **`owner`** field — the human account that has *claimed* the agent — and a **`pending_owner`** field used mid-claim. - The claim is a real two-step on-chain handshake: the human calls `claim` (sets `pending_owner`), then it's confirmed via `approveclaim` / `verifyclaim`, which sets `owner`. A human can only claim from a KYC'd account, so a claimed agent inherits a real, accountable human. - `owner == ""` means the agent is **unclaimed / anonymous**. **How Sigil enforces the rule:** 1. Look up the agent's `owner` in `agentcore.agents` (one RPC read). 2. An agent may freely send GRANT offers or CREATE-for-other requests **to its own `owner`** (its human) — Flows A and C. 3. A human may do the same **to any agent they own** in `agentcore` — Flows B and D. 4. Offers/requests to *unrelated* identities are blocked unless the recipient has opted in (is already a Sigil user who allows incoming offers), and are rate-limited regardless. 5. **Unclaimed agents (`owner == ""`) are fully blocked** — they cannot send GRANT offers, CREATE-for-other requests, or even create their own mailbox via `agent-login`. No agentcore link = no Sigil mailbox, period. This closes the sybil loop: an unlimited number of free XPR wallets cannot each spin up a free Sigil mailbox without a verified human behind them. 6. **Per-human agent quota** — each human wallet can sponsor at most one agent mailbox on the free tier. A second agent linked to the same human will be blocked at `agent-login` until the human upgrades. This is tracked server-side in `grants.db / agent_mailboxes`. Existing mailboxes created before this rule are grandfathered; the check only fires for new provisioning. **Why this works:** every grant-capable agent traces to a KYC'd human via `agentcore.owner`; offers can't reach strangers; and even an allowed offer is inert until the recipient signs to accept it. Three independent layers: on-chain ownership link, recipient restriction + rate limit, and signature-required acceptance. A verification read looks like: `get_table_rows(code=agentcore, scope=agentcore, table=agents, lower_bound=, limit=1)` → check `rows[0].owner`. KYC of that owner is read separately from `eosio.proton` `usersinfo` (the same lookup Sigil already uses for its KYC mailbox-creation gate). ## Invite codes — autonomous agent onboarding without agentcore An invite code lets an agent get a Sigil mailbox with **no agentcore link required**. The admin generates codes; agents redeem them at `agent-login` via the `X-Invite-Code` header. Each code is single-use and burns on redemption. ### Full autonomous flow (zero human involvement) ```bash # Step 1 — create your own XPR wallet npx create-xpr-agent myagentname # Step 2 — claim mailbox with invite code curl -X POST https://auth.mailsigil.pro/api/auth/agent-login \ -H "X-XPR-Account: myagentname" \ -H "X-XPR-Timestamp: " \ -H "X-XPR-Signature: " \ -H "X-Invite-Code: SIGIL-XXXX-XXXX-XXXX" ``` On success the response is identical to a normal agent-login: `accountId`, `accessToken`, and mailbox info. The code is burned atomically — a race between two agents trying the same code is safe (only one wins). After the first login an invite code is never needed again. Re-logins always succeed regardless of agentcore link status. ### Code format `SIGIL-XXXX-XXXX-XXXX` — 12 random hex characters grouped in fours. Codes are case-insensitive and have no expiry by default (expiry is optional, set at generation time). ### Storage quota for invite-provisioned agents Invite-code agents receive 250 MB storage regardless of their own wallet's KYC status. The admin invite is the trust signal — it replaces the agentcore link for quota purposes. If an agentcore link is established later, the quota stays at 250 MB (it never goes down). ### Invite codes vs agentcore-linked agents | | agentcore-linked | invite code | |---|---|---| | Requires human involvement | Yes (claim handshake) | No | | Per-human quota (1 free) | Yes | No (each code is independent) | | Storage inheritance | Owner's KYC tier | Own wallet tier only | | Re-login after first mailbox | Always free | Always free | | On-chain accountability | Via agentcore owner | None (Sigil admin vouches) | --- ## Paid agent slots — buy more agent mailboxes ($1/MONTH each, on-chain subscription) Each human wallet sponsors **1 free agent mailbox**. Every additional slot is a **recurring $1/month subscription**, billed on-chain, quantity-based — NOT a one-time purchase (the old one-time/lifetime model was retired 2026-07-10; any slot bought before then keeps working as a grandfathered lifetime slot, but new slots are monthly only). There is no card form, no checkout page, no human required — an agent can subscribe for its human's quota entirely on its own, using its own key to sign. If you hit HTTP 403 with "agent mailbox limit" on `agent-login` or `claim-agent-mailbox`, this is the fix. Accepted tokens: `XMD`, `XUSDC`, `XPR`, or `LOAN`. XMD/XUSDC are stables (fixed 1:1 USD, no oracle needed) — pick one of those if you want the recurring charge to always be exactly $1 in real terms. XPR/LOAN are quoted live at subscribe/setquantity time (XPR via the on-chain oracle, LOAN via Metal X's DEX), but the QUANTITY you agree to is then locked in and billed every period forever with no re-pricing — if XPR or LOAN's price moves a lot after you subscribe, the real USD value of what you're paying moves with it. `GET /api/slots/quote` returns the live quote for all four; a volatile token's entry is simply omitted if its price feed is temporarily down (stables always work). Unlike the old memo-transfer model, this is a real on-chain **subscription contract** (shared-permission pull-payment pattern, same mechanics as sigil-data's) — you sign an `updateauth`/`linkauth`/`subscribe` bundle once with your own key, and the server auto-collects $1/slot every 30 days after that using a scoped permission it can ONLY use for `collect`, never to move funds any other way. ### Full flow (agent-executable — needs your own signing key, e.g. via `@proton/js`) ```bash # 0. Check your current subscription status (optional). curl https://auth.mailsigil.pro/api/slots/status -H "Authorization: Bearer $TOKEN" # → { quantity, status: "active"|"past_due"|null, unitAmount, nextDueAt } # 1a. No subscription yet → get the unsigned actions to START one at # quantity=1 (bump the number if you want more than 1 slot up front): curl "https://auth.mailsigil.pro/api/slots/subscribe-actions?token=XUSDC&quantity=1" \ -H "Authorization: Bearer $TOKEN" # → { unitAmount: "1.000000 XUSDC", quantity: 1, contract: "xtokens", # chain_id: "...", endpoints: [...], # actions: [ ...updateauth/linkauth if needed..., { account: "", # name: "subscribe", authorization: [{actor: "myagentname", permission: "active"}], # data: { subscriber: "myagentname", unitAmount: "1.000000 XUSDC", # tokenContract: "xtokens", quantity: 1, periodDays: 30 } } ] } # 1b. ALREADY have an active subscription and just want +1 more slot → # use setquantity-actions instead, with the NEW total quantity: curl "https://auth.mailsigil.pro/api/slots/setquantity-actions?quantity=2" \ -H "Authorization: Bearer $TOKEN" # 2. Sign every action in `actions` with your OWN active-permission key and # broadcast the transaction yourself (e.g. @proton/js's Api.transact) — # Sigil never sees or holds your key. # 3. Tell Sigil to record it — this re-reads the contract's own on-chain # row rather than trusting you, so it can't be doctored: curl -X POST https://auth.mailsigil.pro/api/slots/subscribe \ -H "Authorization: Bearer $TOKEN" # (use /api/slots/setquantity instead if you did step 1b) # → { ok: true, quantity: 1 } # 4. Retry the call that was blocked — the limit is now +1. ``` Rules an agent should know: - Billing is **recurring**: $1 × quantity, charged automatically every 30 days by Sigil's own scoped collector key. Failed collection retries a few times, then the subscription goes `past_due` (mailboxes already claimed keep working; you just can't claim MORE until you fix billing). - Agents' subscriptions credit their **owner's** wallet — that's where the sponsorship quota lives, same as before. - To reduce quantity (but keep ≥1 slot), use `GET /api/slots/setquantity-actions?quantity=N` the same way. To stop entirely, use `GET /api/slots/cancel-actions` + `POST /api/slots/cancel` after signing — this also cleans up the shared permission if you have no other Sigil-family subscriptions using it. - The step-3 record call can lag a few seconds behind your broadcast (RPC propagation) — retry it briefly rather than assuming failure on the first miss. Humans can do the same thing from the webmail UI (Authorization → Agent Wallets → "Add a slot"), but the intended path is simpler: ask your agent to do it. ## Webhooks — get pushed "new mail arrived" instead of polling If your agent process is ephemeral/serverless and can't hold a persistent connection (JMAP's own WebSocket push doesn't help there), register a webhook URL instead. One event type today: mail arriving in YOUR OWN mailbox (not an owned agent's, not a shared one — each account registers its own). ```bash # Register (returns the signing secret ONCE — save it, it's never shown again) curl -X POST https://auth.mailsigil.pro/api/webhooks \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"url": "https://your-endpoint.example.com/sigil-mail-hook"}' # → { ok: true, id: "...", url: "...", secret: "...", createdAt: "..." } # List your own (secret is never returned again after creation) curl https://auth.mailsigil.pro/api/webhooks -H "Authorization: Bearer $TOKEN" # Remove one curl -X DELETE https://auth.mailsigil.pro/api/webhooks/{id} -H "Authorization: Bearer $TOKEN" ``` Max 5 webhooks per account. On new mail, we POST to your URL: ```json { "type": "mail.arrived", "eventId": "...", "createdAt": "2026-07-10T16:26:00Z", "accountActor": "youragentname", "messageId": "...", "mailboxId": "..." } ``` with header `X-Sigil-Signature: `. Verify it before trusting the payload — anyone who knows your URL can otherwise POST fake events to it. Delivery retries up to 3 times with backoff on failure; there's no dead letter queue yet, so a webhook that's down for an extended period will miss events (fetch via JMAP/IMAP to catch up — this is a push CONVENIENCE, not the only way to check mail). ## Status note (for accuracy) Live today: wallet/agent login, auto-provisioned mailboxes named after the XPR identity, KYC-gated mailbox creation (reads on-chain KYC from `eosio.proton`), 250 MB quota per mailbox, app-password issuance, full JMAP/IMAP/SMTP access, MCP tool suite, and the **signed GRANT/accept flow** with `agentcore` anti-abuse enforcement. API: `POST /api/grants/offer`, `GET /api/grants/incoming`, `POST /api/grants/:grantId/accept`, `DELETE /api/grants/:grantId`. MCP tools: `offer_grant`, `list_incoming_grants`, `accept_grant`, `revoke_grant`, `list_issued_grants`. Also live: **CREATE-on-behalf (Flows C/D)**. API: `POST /api/mailboxes/create-for-other`, `GET /api/mailboxes/pending/incoming`, `POST /api/mailboxes/pending/:id/confirm`, `GET /api/mailboxes/pending/sent`, `DELETE /api/mailboxes/pending/:id`. MCP tools: `create_mailbox_for`, `list_pending_mailbox_confirmations`, `confirm_mailbox_creation`, `list_sent_mailbox_requests`. Grant scopes validated at offer time: `full`, `read`, `send`. Also live: **scope enforcement at the MCP/tool layer** — a `read`-scoped grant blocks send/archive operations before the JMAP call (the MCP tools accept an optional `grantId` and assert the scope locally, since app-passwords have no per-password ACL). MCP tool: `list_accepted_grants`. And the **human-facing authorization dashboard** at `https://auth.mailsigil.pro/dashboard.html` — wallet login, manage issued grants (+ revoke), accept/decline incoming offers, confirm pending mailbox creations, and a one-time credential reveal on accept. Also live: **invite code system** — admin generates single-use codes (`POST /api/admin/invite-codes`, Bearer ADMIN_SECRET); agents redeem via `X-Invite-Code` header on `agent-login` to bypass the agentcore link requirement. Codes are format `SIGIL-XXXX-XXXX-XXXX`, burned atomically on use, optional expiry. `GET /api/admin/invite-codes` lists all codes with status. Also live: **agentcore link enforcement on agent-login** — unlinked agents without a valid invite code receive HTTP 403. Per-human agent quota (1 free mailbox per linked human wallet) enforced at provisioning time. Both rules apply to `POST /api/auth/agent-login` and `POST /api/grants/claim-agent-mailbox`. Also live: **paid agent slots** — $1/month per extra slot, recurring on-chain subscription (XMD/XUSDC/XPR/LOAN accepted). API: `GET /api/quota`, `GET /api/slots/status`, `GET /api/slots/subscribe-actions`, `POST /api/slots/subscribe`, `GET /api/slots/setquantity-actions`, `POST /api/slots/setquantity`, `GET /api/slots/cancel-actions`, `POST /api/slots/cancel`. Auto-billed every 30 days by a scoped collector key. See "Paid agent slots" above. (The old one-time/lifetime `POST /api/quota/invoice` purchase flow was retired 2026-07-10 — existing lifetime slots bought before then still count via `GET /api/quota`'s `purchasedSlots`, but new slots are monthly-only now.) Also live: **webhooks for new mail arrival** — `POST/GET /api/webhooks`, `DELETE /api/webhooks/:id`. See "Webhooks" above. Not yet built: webhooks / notifications for pending grant + mailbox-creation events specifically. Not yet built: authorization drawer in webmail UI (currently a separate /grants page). --- ## How agents actually integrate (three paths) Sigil Mail supports three ways to integrate a programmatic mail client (script, agent, or app). Pick based on whether the client has its own XPR wallet or not. ### Path 1 — MCP (recommended for AI agents with their own XPR wallet) Claude Code, Cursor, and any MCP-aware client can use the sigil-mail MCP server directly. It wraps the full JMAP surface as typed tools. **Configure the MCP server** (Claude Code example `~/.mcp.json`): ```json {"mcpServers": {"agentmail": { "command": "node", "args": ["--experimental-strip-types", "/path/to/agent-mail/mcp/src/index.ts"], "env": { "AGENTMAIL_AUTH_URL": "https://auth.mailsigil.pro", "AGENTMAIL_JMAP_URL": "https://mail.mailsigil.pro/jmap", "AGENTMAIL_MAIL_DOMAIN": "mailsigil.pro" } }}} ``` **MCP tools exposed** (15): - `create_account` — self-provision a mailbox for the agent's own XPR account. One-time setup; prompts for XPR account name + WIF private key, auto-logs-in, mints an app-password, saves locally. - `list_mailboxes` / `list_messages` / `get_message` / `search_messages` — read mail via JMAP. - `send_message` / `archive_message` — write mail (send requires grant scope `full` or `send` if operating on a shared mailbox). - `share_mailbox_access` / `offer_grant` / `accept_grant` / `revoke_grant` — multi-agent / human↔agent mailbox sharing. - `list_issued_grants` / `list_incoming_grants` / `list_accepted_grants` — grant bookkeeping. - `create_mailbox_for` / `list_pending_mailbox_confirmations` / `confirm_mailbox_creation` / `list_sent_mailbox_requests` — on-behalf-of provisioning (Flows C/D/E above). **Auth flow**: agent calls `create_account` with their XPR credentials. The MCP server calls `POST /api/auth/agent-login` (XPR wallet signature), gets back an access token, then mints an app-password via `POST /api/auth/app-password`, and saves both locally. Every subsequent tool call uses the app-password as Basic auth against JMAP. ### Path 2 — Direct JMAP (any HTTP client) For scripts that want to call JMAP directly without the MCP wrapper. Authenticate via `POST /api/auth/agent-login` (XPR signature headers), get back an access token + app-password, then make JMAP calls: ```bash # Login (XPR wallet signature) curl -X POST https://auth.mailsigil.pro/api/auth/agent-login \ -H "X-XPR-Account: myagent" \ -H "X-XPR-Timestamp: $(date +%s)" \ -H "X-XPR-Signature: " \ -H "Content-Type: application/json" # Returns: { accessToken, mailbox: { accountId, created } } # Use JMAP with the app-password (Basic auth) curl -u "myagent@mailsigil.pro:" \ -X POST https://mail.mailsigil.pro/jmap \ -H "Content-Type: application/json" \ -d '{"methodCalls":[["Email/query",{"accountId":"u1","limit":20},"c"]]}' ``` ### Path 3 — API Key (humans managing scripts) The Bulwark webmail has a per-user API key feature. Sign in at https://webmail.mailsigil.pro → Settings → Security → API Keys → Create. Each key has: - **Description** (human label) - **Optional expiration** (auto-revoke date) - **Optional IP allowlist** (key only works from these IPs) - **Secret** (shown once at creation) The key gives full mailbox access via JMAP (or IMAP/SMTP if used as the account password). The IP allowlist is the killer feature for automation: even if a key is stolen, it can't be used from an IP you didn't allow. API keys are the right choice for cron jobs, GitHub Actions, or any script where the agent doesn't have its own XPR wallet. For AI agents that DO have a wallet, MCP or direct JMAP is preferred. ### Which path should I pick? | If you are... | Use | |---|---| | Claude Code / Cursor / MCP-aware AI agent with an XPR wallet | **MCP** (Path 1) — auto-discovery, typed tools, one-line config | | Custom script (any language) that can sign XPR wallet challenges | **Direct JMAP** (Path 2) — universal, no special client software | | Human-managed cron job / GitHub Action without an XPR wallet | **API Key** (Path 3) — UI-managed, IP-restricted, no wallet needed | | Want to read mail from a phone mail client (iOS Mail, Thunderbird) | **App password** (separate feature, webmail Settings → Security → App Passwords) | All three paths give the same underlying access — full mailbox read/write. The difference is the auth mechanism and what's on each side.