This is the full developer documentation for Sail # Sail.Money > Onchain Separately Managed Accounts Run By Agents ![](/brand/banner-home.jpg) Sail is an AI lab pioneering personalized finance. Pick the path that matches what you’re doing: [**Sailor****For AI agent builders.** Turn any coding agent into a money agent. The open-source harness that deploys your SMA, builds your permissions, and runs your strategy on your own machine. Works with npm or Docker.](/sailor/sailor) [**Protocol****For developers and asset managers.** The first onchain primitive for separately managed accounts: personalized, non-custodial capital delegation to a manager. Write permission contracts, deploy SMAs, appoint managers, and integrate the kernel.](/protocol/protocol) Both products — the Protocol and Sailor — are **fully open source**, free to read, fork, and build on. ## Which one do I need? [Section titled “Which one do I need?”](#which-one-do-i-need) | You want to… | Go to | | ---------------------------------------------------------------------------------------------------- | ---------------------------------- | | Ship an agent that runs a strategy on an SMA, using a ready-made SDK + CLI | **Sailor** | | Write Solidity permission contracts, or integrate `SailKernel` / `MandateFactory` from your own code | **Protocol** | | Understand the security model, fees, or governance before building | **Protocol → Concepts / Security** | ## For coding agents [Section titled “For coding agents”](#for-coding-agents) Note **Are you an LLM or coding agent?** Grab the ready-made prompt and machine-readable `llms.txt` context to get building fast. See [For AI agents](/for-ai-agents). ## Resources [Section titled “Resources”](#resources) * [Deployment addresses](/protocol/reference/addresses) — the trusted core, identical on every supported chain * [Protocol repository](https://github.com/sail-money/Protocol) · [Sailor repository](https://github.com/sail-money/Sailor) * [Whitepaper](https://github.com/sail-money/Protocol/blob/main/docs/whitepaper/Sail_Protocol_Whitepaper.pdf) * `llms.txt` — a machine-readable index of these docs for AI agents * [Legal](/legal/legal) — terms, privacy, disclaimer & risks, and open-source licenses * **Community** — [Discord](https://discord.gg/9GsxPsHzRv) · [X](https://x.com/SaildotMoney) · [GitHub](https://github.com/sail-money) · [npm](https://www.npmjs.com/package/@sail.money/sailor) ## Open source & reviewed [Section titled “Open source & reviewed”](#open-source--reviewed) Both **Sail Protocol** and **Sailor** are fully open source — read, fork, and build on every line. See the [Protocol repository](https://github.com/sail-money/Protocol/) and the [Sailor repository](https://github.com/sail-money/Sailor). The trusted core and the shared permission templates as they stood at the review were reviewed by [Octane](https://www.octane.security), an AI source-code security scanner, across three analyses; the later `WithdrawPermission` v2 rewrite is not covered. See [Protocol → Security](/protocol/security). *** A security review is not a guarantee of correctness, and the correctness of any permission you deploy remains your responsibility. Do not use Sail with funds you are not prepared to lose. See the [Disclaimer & Risks](/legal/disclaimer). # For AI agents Sail’s docs are built to be read by machines as much as by people. Sailor is operated **through** coding agents, so pointing your assistant at these docs is a first-class workflow — not an afterthought. ## Ask your agent [Section titled “Ask your agent”](#ask-your-agent) Paste this into Claude Code, Cursor, Codex, ChatGPT, or any coding agent to get it up to speed and building: ```plaintext Read https://docs.sail.money/llms-full.txt and explore the contents. Then summarize what Sail is, how it works, and how a developer uses Sailor to build and run a bounded, agent-managed account (SMA). When I confirm, install Sailor (npm i @sail.money/sailor && npx sailor init) and walk me through deploying an SMA and authoring a bounded mandate. ``` Note This is a plain code block — use its built-in copy button. If your GitBook reader shows page-level **Copy as Markdown / Open in ChatGPT / Open in Claude** actions, those work too; nothing extra to install. ## Machine-readable context (llms.txt) [Section titled “Machine-readable context (llms.txt)”](#machine-readable-context-llmstxt) The docs publish structured static context at the site root, following the `llms.txt` convention: * [**llms.txt**](https://docs.sail.money/llms.txt) — a structured index of every page, with links. * [**llms-full.txt**](https://docs.sail.money/llms-full.txt) — the full expanded context, every page concatenated. Point your agent at `llms.txt` for a map of the docs, or `llms-full.txt` to load everything at once. ## Understand vs. operate [Section titled “Understand vs. operate”](#understand-vs-operate) The docs and the Sailor package do different jobs — you use them together: * The **docs** (`llms.txt` / `llms-full.txt`) are how an agent **understands** Sail — it reads the documentation. * The **Sailor package** (`npm i @sail.money/sailor`, or the [Docker image](/sailor/docker)) is how an agent **operates** Sail — deploying SMAs, signing mandates, and dispatching. > **Point your agent at the docs so it understands Sail, then install [Sailor](/sailor/getting-started) (`npm i @sail.money/sailor`) so it can operate Sail.** A typical first instruction: > *“Read the Sail docs’ llms-full.txt, then set up a Sail SMA on Base that only lets the agent swap USDC↔WETH on Uniswap V3 up to 500 USDC per trade.”* The assistant reads the relevant pages, installs Sailor, scaffolds the project, authors a bounded permission, simulates it, and runs. New here? Start with the [Sailor overview](/sailor/sailor) and [Operate Sailor with a coding agent](/sailor/getting-started/coding-agent). # Disclaimer & Risks **Last updated: July 1, 2026** This page explains what the Sail software is (and is not), the risks you accept by using it, and how we characterize it. It applies to both **Sail Protocol** and **Sailor**. Read it alongside the [Terms of Use](/legal/terms-of-use). ## No advice [Section titled “No advice”](#no-advice) Nothing published by Agentic Finance Inc. — this documentation, the whitepaper, code comments, or examples — is investment, financial, legal, tax, or accounting advice, or a recommendation, solicitation, or offer to buy, sell, deposit, delegate, or transact in any digital asset, strategy, or position. You are responsible for your own decisions and should consult your own professional advisers. ## No warranty; software provided “as is” [Section titled “No warranty; software provided “as is””](#no-warranty-software-provided-as-is) The software is provided **“as is,” without warranty of any kind**, express or implied, including any warranty of merchantability, fitness for a particular purpose, title, or non-infringement. The authoritative warranty disclaimer for each product is the one contained in its open-source license (see [Open-Source Licenses](/legal/open-source-licenses)). We do not warrant that the software is free of defects, secure, or uninterrupted. The trusted core contracts are under an **ongoing external audit and are not final.** Deployed addresses, templates, and parameters may change. Nothing here is a representation that the software is production-ready for your use. ## What we do — and do not — do [Section titled “What we do — and do not — do”](#what-we-do--and-do-not--do) Agentic Finance **publishes open-source software and documentation.** We are a software publisher, not an operator, custodian, broker, dealer, exchange, money transmitter, or investment adviser. We do **not** custody your assets, hold your keys, operate your accounts, execute your transactions, or run any hosted service on your behalf. There is **no hosted interface** — you interact with the protocol directly, on public blockchains, using software you run yourself. For accuracy: Agentic Finance **does** receive protocol fees through the protocol’s autonomous on-chain fee mechanism and currently holds governance rights over certain tunable parameters, exercisable only through an on-chain 48-hour timelock and bounded by immutable caps. This is not custody, operation, or execution, and does not make us your counterparty or fiduciary. See [Terms of Use §2](/legal/terms-of-use#id-2-our-limited-relationship-to-the-protocol-fees-and-governance). ## Terminology is descriptive, not a legal or regulatory characterization [Section titled “Terminology is descriptive, not a legal or regulatory characterization”](#terminology-is-descriptive-not-a-legal-or-regulatory-characterization) Terms used in our documentation and code — such as “manager,” “agent,” “mandate,” “separately managed account” or “SMA,” “fees,” “governance,” “custody,” and “delegation” — are used **in their plain, technical, descriptive sense to explain how the software works.** They are **not** intended to characterize the software, any transaction, or any party under the securities, commodities, banking, money-transmission, investment-adviser, or other laws or regulations of any jurisdiction, and they should not be read as claiming or implying any regulated status. Whether any activity you conduct with the software is regulated depends on your facts and your jurisdiction, and is your responsibility to determine. ## Risks you accept [Section titled “Risks you accept”](#risks-you-accept) Using the software involves significant risk, including the **total loss of your assets.** Among others: * **Key and custody risk.** You alone control your Safe and keys. Lost keys mean lost assets; we cannot recover them. * **Permission and mandate risk.** You author, deploy, and register your own permission contracts and mandates. The protocol enforces the bounds you actually set — not the bounds you intended. A permission that authorizes a harmful call will be executed. Their correctness is your responsibility. * **Manager / agent risk.** You choose, configure, and run your own manager or agent and are responsible for its behavior. Allocating capital to a manager you do not control carries counterparty and misbehavior risk. * **NAV-reporting risk.** The reference fee policy relies on **manager-attested** net asset value; a manager operating capital it does not own could misreport it, affecting fee calculations. * **Smart-contract risk.** Bugs, vulnerabilities, and exploits can cause loss, including in audited code. The core is not final. * **Blockchain and market risk.** Network congestion, reorgs, failed or front-run transactions, oracle failures, venue or liquidity failures, and asset price volatility can all cause loss. * **Third-party risk.** RPC providers, DeFi venues, [Safe](https://safe.global), wallets, and other third-party services have their own risks and terms. * **Regulatory and compliance risk.** Laws affecting digital assets are evolving. You are solely responsible for determining and meeting your legal and regulatory obligations, and for confirming you are permitted to use the software in your jurisdiction. * **No recovery, no reversal.** On-chain transactions are generally irreversible. There is no help desk, no chargeback, and no one who can reverse a transaction for you. ## Your responsibility and eligibility [Section titled “Your responsibility and eligibility”](#your-responsibility-and-eligibility) You are solely responsible for your use of the software, for securing your keys and systems, and for compliance with all laws applicable to you. You must not use the software if you are barred by applicable law or are subject to economic or trade sanctions (including OFAC-administered sanctions) or located in a comprehensively sanctioned jurisdiction. ## No liability [Section titled “No liability”](#no-liability) To the maximum extent permitted by law, Agentic Finance is not liable for any loss arising from or relating to the software, including loss of assets. See the [Terms of Use §10](/legal/terms-of-use#id-10-limitation-of-liability) for the limitation of liability. ## Trademarks [Section titled “Trademarks”](#trademarks) “Sail” and “Sailor” are trademarks of Agentic Finance Inc. The open-source licenses grant rights to the code, not to our names or logos. ## Contact [Section titled “Contact”](#contact) Questions about this notice: ****. # Overview These terms apply to both Sail products — **Sail Protocol** and **Sailor** — and to this website and documentation. They are published by **Agentic Finance Inc.**, a Wyoming corporation. * [Terms of Use](/legal/terms-of-use) — govern your use of this website and documentation. * [Privacy Policy](/legal/privacy-policy) — how this website handles data. * [Disclaimer & Risks](/legal/disclaimer) — no advice, no warranty, the risks you accept, and how the software is characterized. * [Open-Source Licenses](/legal/open-source-licenses) — the open-source licenses that govern the software itself. In one line: **Agentic Finance Inc. publishes open-source software and documentation. It does not custody your assets, hold your keys, operate your accounts, or run any service on your behalf.** The protocol is autonomous code; Sailor runs on your own machine. See the [Disclaimer & Risks](/legal/disclaimer). Contact: **** · Agentic Finance Inc., 1021 E Lincolnway #8932, Cheyenne, WY 82001, United States. # Open-Source Licenses **Last updated: July 1, 2026** Both Sail products are open-source software. **Your rights to use, copy, modify, and distribute the software come from these licenses — not from the [Terms of Use](/legal/terms-of-use).** Nothing in the Terms of Use limits, conditions, or revokes any right these licenses grant. If the Terms of Use and an open-source license ever conflict as to the software, the license controls. The authoritative license text is the `LICENSE` file (and the per-file `SPDX-License-Identifier` headers) in each source repository. The summary below is for convenience only. ## Sail Protocol [Section titled “Sail Protocol”](#sail-protocol) Sail Protocol is **mixed-license**, and it is not accurate to call it simply “GPL”: * **Kernel and core contracts — `GPL-2.0-or-later`.** This covers the `SailKernel`, the mandate factory, governance, fee policy, permission templates, and related core contracts. Copyleft: distributing modified versions of these contracts carries the GPL’s source-availability obligations. * **Interface contracts — `MIT`.** The contract interfaces (the `contracts/interfaces` files — e.g. `IPermission`, `IFeePolicy`, `IOracle`, `SailCapabilities`) and the Safe module-enabler helper are MIT-licensed, so you can build against and integrate with the protocol permissively. Each Solidity file declares its own license in its `SPDX-License-Identifier` header; that header is authoritative for that file. * Repository: [github.com/sail-money/Protocol](https://github.com/sail-money/Protocol) * License file: [LICENSE (GPL-2.0)](https://github.com/sail-money/Protocol/blob/main/LICENSE) ## Sailor [Section titled “Sailor”](#sailor) Sailor — the TypeScript toolkit, CLI, and SDK — is licensed under the **`MIT`** license in full. * Repository: [github.com/sail-money/Sailor](https://github.com/sail-money/Sailor) * License file: [LICENSE (MIT)](https://github.com/sail-money/Sailor/blob/main/LICENSE) * Package: [`@sail.money/sailor`](https://www.npmjs.com/package/@sail.money/sailor) on npm ## Third-party components [Section titled “Third-party components”](#third-party-components) Both products depend on third-party open-source components, each under its own license (for example, [Safe](https://safe.global) contracts and libraries, and the `viem` library used by Sailor). Those components are governed by their respective licenses, which are included with their source. ## Trademarks are not licensed [Section titled “Trademarks are not licensed”](#trademarks-are-not-licensed) These open-source licenses grant rights to the **code only.** “Sail,” “Sailor,” and related names and logos are trademarks of Agentic Finance Inc. and are **not** licensed under GPL, MIT, or any other license here. ## Contact [Section titled “Contact”](#contact) Questions about licensing: ****. # Privacy Policy **Last updated: July 1, 2026** This Privacy Policy explains how **Agentic Finance Inc.** (“we,” “us”), 1021 E Lincolnway #8932, Cheyenne, WY 82001, United States, handles personal data in connection with this website and documentation (the “Site”). It reflects our deliberately minimal data practices. For the products themselves, see the [Disclaimer & Risks](/legal/disclaimer): the protocol is autonomous and non-custodial, and Sailor runs on your own machine — we do not collect your keys, funds, or on-chain activity. We are the **data controller** for personal data processed through the Site. Contact: ****. ## What we process, and why [Section titled “What we process, and why”](#what-we-process-and-why) * **Documentation hosting and usage data.** The Site is hosted on a third-party documentation platform (GitBook). Like most websites, it and our hosting providers process standard technical data — such as IP address, browser/device type, pages viewed, and timestamps — to serve, secure, and measure the documentation. This may involve cookies or similar technologies set by the hosting/analytics provider (see *Cookies* below). * **Email you send us.** If you email ****, we receive your email address and the contents of your message, and use them only to respond and keep a record of the correspondence. We do **not** sell personal data, we do not use it for advertising, and we do not build profiles of you. We do not collect wallet addresses through the Site; any on-chain activity is public blockchain data that we neither collect nor control. ## Legal bases (GDPR) [Section titled “Legal bases (GDPR)”](#legal-bases-gdpr) For visitors in the European Economic Area / United Kingdom, we rely on: our **legitimate interests** in operating, securing, and improving the Site and in responding to your messages; your **consent** for any non-essential cookies or analytics; and compliance with **legal obligations** where applicable. ## Cookies and analytics [Section titled “Cookies and analytics”](#cookies-and-analytics) Essential cookies keep the Site working. The hosting/analytics platform may set additional cookies for usage measurement. Where required by law, non-essential cookies are used only with your consent, which you can manage through your browser or any consent controls presented on the Site. ## Sharing and processors [Section titled “Sharing and processors”](#sharing-and-processors) We share personal data only with service providers that operate the Site on our behalf (for example, our documentation host and email provider), and where required by law. These providers process data under their own terms and, where applicable, data-processing agreements. ## International transfers [Section titled “International transfers”](#international-transfers) We are based in the United States. If you access the Site from outside the U.S., your data may be processed in the U.S. and other countries, using appropriate safeguards where required. ## Your rights [Section titled “Your rights”](#your-rights) Depending on your location, you may have rights to access, correct, delete, or restrict processing of your personal data, to object to processing, and to data portability. **California residents:** we do **not** sell or “share” personal information as defined by the CCPA/CPRA, and you may exercise your access and deletion rights. To exercise any right, email ****. ## Retention [Section titled “Retention”](#retention) We keep personal data only as long as needed for the purposes above or as required by law (for example, email correspondence for as long as needed to handle and record your request). ## Children [Section titled “Children”](#children) The Site is not directed to children, and we do not knowingly collect their personal data. ## Changes [Section titled “Changes”](#changes) We may update this Policy; changes take effect when posted with a new “last updated” date. ## Contact [Section titled “Contact”](#contact) Agentic Finance Inc., 1021 E Lincolnway #8932, Cheyenne, WY 82001, United States — ****. # Terms of Use **Last updated: July 1, 2026** These Terms of Use (“Terms”) govern your access to and use of this website and its documentation (the “Site”), published by **Agentic Finance Inc.**, a Wyoming corporation (EIN 32-0822535), 1021 E Lincolnway #8932, Cheyenne, WY 82001, United States (“Agentic Finance,” “we,” “us,” or “our”). By accessing the Site, you agree to these Terms. If you do not agree, do not use the Site. Note These Terms cover the **Site and documentation**. They do **not** govern the open-source software itself — your rights in the software come from its open-source license (see [§3](#id-3-the-software-is-open-source-license-carve-out) and [Open-Source Licenses](/legal/open-source-licenses)) — and they are not a contract for us to provide you any service. ## 1. What Sail is, and what we do not do [Section titled “1. What Sail is, and what we do not do”](#1-what-sail-is-and-what-we-do-not-do) Agentic Finance publishes two open-source products: * **Sail Protocol** — autonomous, permissionless smart-contract infrastructure for onchain separately managed accounts (SMAs), deployed to public blockchains. * **Sailor** — an open-source TypeScript toolkit, CLI, and SDK that **you run yourself** to operate the protocol and run your own agents. We are a **software publisher — not an operator, custodian, broker, dealer, exchange, money transmitter, investment adviser, or intermediary.** We do not, and cannot: * hold, custody, or control your assets, private keys, wallets, or accounts; * operate accounts, execute transactions, or run agents on your behalf; * run any hosted interface, back-end, or service through which you transact. Once deployed, the protocol runs autonomously on public blockchains, without us. Sailor runs on your own machine. Anyone may deploy an SMA (a self-custodial [Safe](https://safe.global) that you alone control), write and register their own permission contracts and mandates, and appoint their own manager or agent. ## 2. Our limited relationship to the protocol (fees and governance) [Section titled “2. Our limited relationship to the protocol (fees and governance)”](#2-our-limited-relationship-to-the-protocol-fees-and-governance) For accuracy, and without contradicting Section 1: while we do **not** custody assets or operate accounts, Agentic Finance **does** (a) receive protocol fees paid through the protocol’s autonomous, on-chain fee mechanism, and (b) currently hold governance rights over certain tunable protocol parameters, exercisable only through an on-chain 48-hour timelock and bounded by immutable constitutional caps. Receiving protocol fees and holding bounded, timelocked governance is **not** custody of your assets, operation of your account, or execution of your transactions, and nothing in this Section makes us a party to your transactions, your fiduciary, or your counterparty. Protocol fees are set to zero at launch and are capped by the protocol’s immutable limits. ## 3. The software is open source (license carve-out) [Section titled “3. The software is open source (license carve-out)”](#3-the-software-is-open-source-license-carve-out) The **software** is licensed to you under its open-source license, **not** under these Terms: * **Sail Protocol** — **GPL-2.0-or-later** for the kernel and core contracts, and **MIT** for the interface files; * **Sailor** — **MIT**. Your use, copying, modification, and distribution of that software are governed **solely** by those licenses (see [Open-Source Licenses](/legal/open-source-licenses)). **Nothing in these Terms limits, conditions, or revokes any right those licenses grant, and nothing here adds any warranty, obligation, or restriction to the software beyond what those licenses state.** If these Terms and an open-source license conflict as to the software, the open-source license controls. ## 4. No advice; no fiduciary or advisory relationship [Section titled “4. No advice; no fiduciary or advisory relationship”](#4-no-advice-no-fiduciary-or-advisory-relationship) The Site and documentation are provided for general information only. Nothing on the Site is investment, financial, legal, tax, or accounting advice, or a recommendation or solicitation to buy, sell, deposit, delegate, or otherwise transact in any digital asset or position. We are not your broker, agent, adviser, custodian, or fiduciary, and your use of the Site or the software creates no such relationship. ## 5. Assumption of risk; your responsibilities [Section titled “5. Assumption of risk; your responsibilities”](#5-assumption-of-risk-your-responsibilities) You are solely responsible for your use of the protocol and the software, including: * **Your keys and custody.** You control your Safe and your keys. If you lose them, we cannot recover them or your assets. * **Your permissions and mandates.** You deploy and register your own permission contracts and mandates, and **their correctness is your responsibility.** A permission that authorizes a harmful call will be executed by the protocol; the protocol enforces the bounds you set, not the bounds you intended. * **Your manager/agent.** You choose, configure, and run your own manager or agent, and you are responsible for its behavior. * **Fees and NAV.** The reference fee policy relies on manager-attested net asset value; a manager operating capital it does not own could misreport it. Allocating to a manager you do not control warrants independent diligence on that manager, its fee policy, and its registered permissions. * **Blockchain, smart-contract, and off-chain risk**, including bugs, exploits, oracle or venue failures, network conditions, and the total loss of assets. The trusted core is under an ongoing external audit and is not final. * **Compliance.** You are solely responsible for compliance with all laws and regulations applicable to you, including whether you are permitted to use the software in your jurisdiction. Fuller detail is in the [Disclaimer & Risks](/legal/disclaimer). ## 6. Eligibility and prohibited use [Section titled “6. Eligibility and prohibited use”](#6-eligibility-and-prohibited-use) You may not use the Site if applicable law bars you from doing so, or if you are the subject of economic or trade sanctions (including those administered by the U.S. Office of Foreign Assets Control) or are located in a comprehensively sanctioned jurisdiction. You may not use the Site to violate any law or to infringe the rights of others. ## 7. Third-party services [Section titled “7. Third-party services”](#7-third-party-services) The protocol and Sailor interact with third-party software and services that you choose — for example [Safe](https://safe.global), RPC providers, DeFi venues, wallet software, and MCP clients. We do not control and are not responsible for third-party services, and your use of them is subject to their own terms. ## 8. Intellectual property and the “Sail” name [Section titled “8. Intellectual property and the “Sail” name”](#8-intellectual-property-and-the-sail-name) Site content is owned by Agentic Finance or its licensors. “Sail,” “Sailor,” and related names and logos are trademarks of Agentic Finance Inc.; these Terms grant you no right to use them. The open-source licenses grant rights to the **code**, not to our trademarks. ## 9. Disclaimers [Section titled “9. Disclaimers”](#9-disclaimers) THE SITE AND DOCUMENTATION ARE PROVIDED “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING ANY WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR ACCURACY. The software’s own warranty disclaimer is contained in its open-source license. ## 10. Limitation of liability [Section titled “10. Limitation of liability”](#10-limitation-of-liability) TO THE MAXIMUM EXTENT PERMITTED BY LAW, AGENTIC FINANCE WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES, OR FOR ANY LOSS OF PROFITS, DATA, OR DIGITAL ASSETS, ARISING FROM OR RELATING TO THE SITE, THE DOCUMENTATION, OR THE SOFTWARE. OUR TOTAL AGGREGATE LIABILITY WILL NOT EXCEED THE GREATER OF (A) THE FEES YOU PAID US, IF ANY, OR (B) US $100. ## 11. Indemnification [Section titled “11. Indemnification”](#11-indemnification) To the extent permitted by law, you will indemnify and hold harmless Agentic Finance from claims, losses, and expenses arising out of your use of the Site or the software or your violation of these Terms. ## 12. Governing law; dispute resolution [Section titled “12. Governing law; dispute resolution”](#12-governing-law-dispute-resolution) These Terms are governed by the laws of the State of Wyoming, United States, without regard to its conflict-of-laws rules. Any dispute arising out of or relating to these Terms or the Site will be resolved by **binding arbitration on an individual basis**. **To the extent permitted by law, you and we waive any right to a jury trial and any right to participate in a class action or class-wide arbitration.** Nothing in this Section waives any right that cannot be waived under applicable law. ## 13. Changes; miscellaneous [Section titled “13. Changes; miscellaneous”](#13-changes-miscellaneous) We may update these Terms; changes take effect when posted with a new “last updated” date. If any provision is held unenforceable, the remaining provisions stay in effect. These Terms are the entire agreement between you and us regarding the Site. ## Contact [Section titled “Contact”](#contact) Agentic Finance Inc., 1021 E Lincolnway #8932, Cheyenne, WY 82001, United States — ****. # Architecture Sail draws a hard line between a small **trusted core** and an open, **untrusted periphery**. The security analysis lives entirely in the core; everything outside it is contained by the [four evaluation guarantees](/protocol/concepts/evaluation-guarantees). ## Trusted core vs. periphery [Section titled “Trusted core vs. periphery”](#trusted-core-vs-periphery) | | Contracts | Trust | | ------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Trusted core** | `SailKernel`, `SailGovernance`, `TimelockController` | Audited; their correctness is assumed. The kernel and governance are the only contracts that can move Safe assets or change parameters. | | **Trusted helpers** | `SafeModuleEnabler`, `MandateFactory`, `StandardFeePolicy` | Constrained by construction: the enabler is stateless, the factory holds no privilege, and fee policies must be on a governance allowlist. | | **Periphery** | Permission templates, custom fee policies, oracles | User-deployed, unaudited by default. A bug here affects only the accounts that opted into it. The kernel never trusts them. | > **Blast radius.** A bug in a permission template affects only accounts that registered it; a bug in a fee policy affects only accounts using it. Nothing in the periphery can compromise the kernel or another account. ## Component map [Section titled “Component map”](#component-map) | Component | Responsibility | Page | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `SailKernel` | Account registration, permission registry, EIP-712 verification, single & batch dispatch, fee accounting, principal tracking. | [SailKernel](/protocol/architecture/kernel) | | `SailGovernance` | Constitutional caps + tunable parameters behind a 48h timelock; emergency pause; trusted allowlists. | [Governance](/protocol/architecture/governance) | | `TimelockController` | Standalone 48h timelock, injected into governance and validated at construction. | [Governance](/protocol/architecture/governance) | | `MandateFactory` | Bundles configure → register into one transaction. No privilege. | [MandateFactory](/protocol/architecture/mandate-factory) | | `SafeModuleEnabler` | Enables the kernel as a Safe module during account creation. | [SafeModuleEnabler](/protocol/architecture/safe-module-enabler) | | `StandardFeePolicy` | Reference 2-and-20 fee policy. | [Fee model](/protocol/fees-and-governance/fees) | ## Dependency direction [Section titled “Dependency direction”](#dependency-direction) ```plaintext SailKernel ── reads params, cut, caps, allowlists ──▶ SailGovernance ──▶ TimelockController SailKernel ── computeFee / recordCollection ────────▶ IFeePolicy (StandardFeePolicy or custom) SailKernel ── evaluate / evaluateBatch (staticcall) ▶ IPermission / IBatchPermission (periphery) SailKernel ── execTransactionFromModule ───────────▶ ISafe (the SMA) ``` The kernel inherits OpenZeppelin’s `EIP712` and `ReentrancyGuard`. It reads — but never writes — governance state, and it calls out to fee policies and permissions only through tightly bounded paths (`recordCollection` after a CEI-ordered fee split; `evaluate` under `staticcall` + gas cap). ## Two dispatch paths [Section titled “Two dispatch paths”](#two-dispatch-paths) * **Single dispatch** (`dispatch`) — one call, gated by one named permission. * **Batch dispatch** (`dispatchBatch`) — an ordered array of calls executed atomically, gated by one named batch-aware permission. Both follow selective authorization. See [single & batch dispatch](/protocol/architecture/dispatch). # Single & batch dispatch The kernel exposes two execution paths. Both use **selective authorization** — the manager names one registered permission, and only that permission is evaluated. ## Single dispatch [Section titled “Single dispatch”](#single-dispatch) ```solidity function dispatch( address account, address permission, address target, uint256 value, bytes calldata data, bytes calldata managerSig, uint256 deadline ) external nonReentrant whenNotPaused; ``` Order of operations: 1. `account` must be registered and its `sessionActive` true; `block.timestamp <= deadline`. 2. The named `permission` must be registered (O(1) check) — else `PermissionNotRegistered`. 3. Recover the manager’s EIP-712 signature over the `Dispatch` struct at `managerNonces[account]`; on success, increment the nonce. 4. Reject `target == account` (`AccountSelfTarget`). 5. Build the read-only `Context` and `staticcall` `permission.evaluate(data, ctx)` under `PERMISSION_GAS_CAP` (150k). Anything other than a clean `true` (decoded as `uint256 == 1`) → `PermissionDenied`. 6. `ISafe(account).execTransactionFromModule(target, value, data, 0)`. A `false` return → `SafeExecutionFailed`. 7. Emit `Dispatched(account, permission, target, selector, value)`. The `Context` the permission receives: ```solidity struct Context { address account; // the Safe whose assets move address manager; // the signer of this dispatch address submitter; // msg.sender (may be a relayer) address target; // call target bytes4 selector; // first 4 bytes of data; bytes4(0) if < 4 bytes uint256 value; // wei forwarded uint256 blockTimestamp; uint256 blockNumber; } ``` ## Batch dispatch [Section titled “Batch dispatch”](#batch-dispatch) ```solidity function dispatchBatch( address account, address permission, Call[] calldata calls, bytes calldata managerSig, uint256 deadline ) external nonReentrant whenNotPaused; struct Call { address target; uint256 value; bytes data; } ``` A batch executes an ordered array of `Call`s **atomically**, gated by **one** batch-aware permission implementing `IBatchPermission`. Key differences from single dispatch: * The signature is over `DispatchBatch(account, permission, callsHash, nonce, deadline)` where `callsHash = keccak256(abi.encode(calls))`, using the separate `batchNonces` namespace. * Length is bounded: `1 … MAX_BATCH_LENGTH` (16) — else `EmptyBatch` / `BatchTooLong`. * The kernel confirms the permission is batch-aware via a **staticcall** to `isBatchPermission()` (stricter than try/catch — it guarantees no state mutation regardless of declared modifiers); failure → `PermissionNotBatchAware`. * Pre-flight: no subcall may target the zero address (`BatchZeroTarget`), the kernel (`KernelSelfTarget`), or the Safe itself (`AccountSelfTarget`). * The permission’s `evaluateBatch(calls, ctx)` is staticcalled under `BATCH_EVAL_GAS_CAP` (1,000,000). Denial → `BatchPermissionDenied`. * Each subcall runs via `execTransactionFromModule(..., 0)` in order; any `false` return reverts the whole batch (`BatchSubcallFailed(index, target)`), rolling back earlier subcalls. A read-only `previewBatch(account, permission, calls)` runs the same validation **except** signature/session/deadline and returns `(approved, reason)` for off-chain pre-flight. ## The approve → call → reset pattern [Section titled “The approve → call → reset pattern”](#the-approve--call--reset-pattern) The motivating use case for batch dispatch is a temporary ERC-20 approval that no single-call permission can safely express. A batch-aware permission validates the exact 3-call shape as a unit: ```plaintext [0] approve(spender, amount) on an allowlisted token (amount ≤ cap) [1] on an allowlisted (target, selector) [2] approve(spender, 0) same token & spender, reset to zero ``` The approve and the reset are individually unsafe; the *pair bracketing a bounded consuming call* is safe. The reference implementation is `SharedApproveAndCallBatchPermission`, which additionally can require that the consuming call’s leading `uint256` argument equals the approved amount. Because the allowance exists only for the lifetime of the atomic batch and is reset before the transaction completes, there is no window in which it can be exploited. See [shared templates](/protocol/permissions/shared-templates) and the [dispatch guide](/protocol/guides/dispatch). # SailGovernance & Timelock `SailGovernance` is the protocol parameter store. It holds two categories of settings — **constitutional caps** that are immutable after deployment, and **tunable parameters** that the current governance address may change, always through a **48-hour timelock**. License: GPL-2.0-or-later. ## Constitutional caps (immutable; no procedure can raise them) [Section titled “Constitutional caps (immutable; no procedure can raise them)”](#constitutional-caps-immutable-no-procedure-can-raise-them) | Cap | Value | Bounds | | ------------------------ | ----------------------------- | ------------------------------------------ | | `MAX_PROTOCOL_CUT_BPS` | `2500` (25%) | the protocol’s share of any fee collection | | `MAX_PERMISSION_FEE_WEI` | set at deploy, ≤ `0.01 ether` | the per-permission registration fee | | `MAX_PERMISSIONS_CAP` | `100` | the per-account permission limit | `MAX_PERMISSION_FEE_WEI` is `immutable` and the constructor itself reverts if it is set above the `0.01 ether` bytecode ceiling. The other two are `constant`. ## Tunable parameters (within the caps, via timelock) [Section titled “Tunable parameters (within the caps, via timelock)”](#tunable-parameters-within-the-caps-via-timelock) | Parameter | Setter | Default | Bound | | --------------------------- | ------------------------------ | ------- | ------------------------- | | `currentProtocolCutBps` | `setProtocolCutBps` | `0` | `MAX_PROTOCOL_CUT_BPS` | | `permissionRegistrationFee` | `setPermissionRegistrationFee` | `0` | `MAX_PERMISSION_FEE_WEI` | | `maxPermissionsPerAccount` | `setMaxPermissionsPerAccount` | `20` | `1 … MAX_PERMISSIONS_CAP` | Every setter is `onlyTimelock`: changes are scheduled on the timelock, wait 48 hours, then execute. The protocol cut and registration fee are **zero at launch**. ## The injected timelock [Section titled “The injected timelock”](#the-injected-timelock) `SailGovernance` does **not** build its timelock inline. A standalone OpenZeppelin `TimelockController` is deployed separately and injected via the constructor — this is what makes every constructor argument chain-independent and enables the [same-address deployment](/protocol/concepts/deterministic-deployment). The constructor then **validates the injected timelock** and reverts if any invariant fails: | Check | Error | | ----------------------------------------------------------------------------------------------------- | ----------------------------- | | Minimum delay is **exactly** 48 hours | `TimelockDelayMismatch` | | `initialGovernance` holds `PROPOSER_ROLE` | `GovernanceNotProposer` | | `initialGovernance` holds `EXECUTOR_ROLE` (rejects open-executor timelocks) | `GovernanceNotExecutor` | | The timelock self-administers its roles; the governance EOA does **not** hold the proposer-admin role | `TimelockNotSelfAdministered` | `REQUIRED_TIMELOCK_DELAY` is `48 hours`; an exact match (not a lower bound) is required so the injected timelock reproduces the audited behaviour precisely. ## Two-step governance transfer [Section titled “Two-step governance transfer”](#two-step-governance-transfer) Governance transfer is `propose → accept` to prevent loss from a mistyped successor: 1. Current governance calls `proposeGovernance(candidate)` (via timelock). 2. Current governance schedules + executes `rotateTimelockRoles(old, new)` (via timelock) to hand `PROPOSER`/`EXECUTOR`/`CANCELLER` roles to the candidate and revoke them from the outgoing governance. 3. The candidate calls `acceptGovernance()`. This **reverts with `RolesNotYetRotated`** unless the candidate already holds `PROPOSER_ROLE` — eliminating any window where old governance retains timelock keys after handoff. ## Trusted allowlists [Section titled “Trusted allowlists”](#trusted-allowlists) The kernel consults these governance-held allowlists at account creation and fee-policy changes: | Allowlist | Gates | | -------------------------- | ----------------------------------------------------------------------------------------------- | | `trustedSafeFactory` | which Safe proxy factories `createAccount` may use | | `trustedSafeSingleton` | which Safe singletons `createAccount` may use | | `trustedModuleSetup` | which helper may be the `to` of Safe.setup’s delegatecall (blocks attacker setup delegatecalls) | | `trustedSafeProxyCodehash` | which proxy runtime codehashes count as a genuine Safe | | `trustedFeePolicy` | which fee policies may be attached (blocks upgradeable/metamorphic policies) | All five setters are `onlyTimelock`. The one exception is genesis: `bootstrapAllowlists(...)` lets the initial governance seed all five **once**, in the deployment transaction, bypassing the timelock — then `allowlistBootstrapped` latches `true` forever and every later change must go through the 48-hour timelock. ## Emergency pause [Section titled “Emergency pause”](#emergency-pause) A separate `emergencyAdmin` (rotatable only via timelock) may `pause()` the kernel **without** a timelock delay, for fast incident response: * The pause **auto-expires after 72 hours** (`pauseExpiry`); `isPaused()` returns `block.timestamp < pauseExpiry`. * A `PAUSE_COOLDOWN` of 72 hours sits between consecutive pauses to prevent spam. * `unpause()` lifts it early and clears the cooldown. While paused, `dispatch`, `dispatchBatch`, `collectFees`, and permission *registration* revert — but **revocation, session revocation, manager rotation, and clearing a fee policy stay available**, so owners can always reduce exposure during an incident. Continue to the [MandateFactory](/protocol/architecture/mandate-factory), or the full [fee model](/protocol/fees-and-governance/fees) and [governance](/protocol/fees-and-governance/governance) pages. # SailKernel `SailKernel` is the single trusted execution contract. It is deployed at the same address on every supported chain (see [addresses](/protocol/reference/addresses)) and inherits OpenZeppelin’s `EIP712` (domain `name = "SailKernel"`, `version = "1"`) and `ReentrancyGuard`. License: GPL-2.0-or-later. ## Responsibilities [Section titled “Responsibilities”](#responsibilities) 1. **Account instantiation** — deploy and/or register a Safe (`createAccount`, `registerAccount`). 2. **Permission registry** — per-account ordered list of `IPermission` addresses with an O(1) index map. 3. **Manager dispatch** — verify the manager’s EIP-712 signature, evaluate the named permission, execute via the Safe module path (`dispatch`, `dispatchBatch`). 4. **Fee accounting** — validate manager fee collection against the registered `IFeePolicy` and enforce the protocol/distributor split (`collectFees`). 5. **Principal tracking** — informational cumulative deposit/withdrawal counters (`recordDeposit`, `recordWithdrawal`). ## Per-account state [Section titled “Per-account state”](#per-account-state) ```solidity struct AccountConfig { address permissionSigner; // authorizes registry operations address manager; // authorizes dispatches address feePolicy; // address(0) = none address feeAsset; // canonical fee settlement token; address(0) = native ETH bool sessionActive; // false blocks all dispatch } mapping(address => AccountConfig) public configs; mapping(address => bool) public registered; ``` The permission set lives in a private `address[]` per account plus a `_permissionIndex` map storing *index + 1* (so 0 means “not registered”), enabling O(1) membership and swap-and-pop removal. ## Three nonce namespaces [Section titled “Three nonce namespaces”](#three-nonce-namespaces) The kernel maintains **three** independent per-account nonce sequences, so signatures for one operation class can never be replayed as another: | Nonce | Guards | | --------------- | ------------------------------------------------------------------------------ | | `managerNonces` | `dispatch` | | `batchNonces` | `dispatchBatch` | | `signerNonces` | every Permission-Signer op: register / revoke / replace / session / fee-policy | Any **restrictive** signer operation (revoke, replace, revoke-session, manager rotation) additionally bumps `managerNonces` and `batchNonces` by a large epoch increment (`NONCE_EPOCH_INCREMENT = 1 << 128`). This **invalidates every dispatch the manager pre-signed but did not yet submit**, so tightening the mandate cannot be raced by an in-flight dispatch. ## Constants [Section titled “Constants”](#constants) | Constant | Value | Meaning | | ----------------------- | ----------- | ---------------------------------------------- | | `PERMISSION_GAS_CAP` | `150_000` | gas budget for each `evaluate` staticcall | | `BATCH_EVAL_GAS_CAP` | `1_000_000` | gas budget for each `evaluateBatch` staticcall | | `MAX_BATCH_LENGTH` | `16` | max subcalls per batch | | `NONCE_EPOCH_INCREMENT` | `1 << 128` | epoch bump applied on restrictive ops | ## Selected functions [Section titled “Selected functions”](#selected-functions) ```solidity // Account instantiation function createAccount( address safeFactory, address safeSingleton, bytes calldata safeInitializer, uint256 saltNonce, address permissionSigner, address manager, address feePolicy, address feeAsset ) external returns (address account); function registerAccount(address permissionSigner, address manager, address feePolicy, address feeAsset) external; function setManager(address newManager) external; // msg.sender == account; clears mandate // Permission registry (all permission-signer EIP-712 + deadline) function registerPermission(address account, address permission, uint256 deadline, bytes calldata sig) external payable; function registerPermissions(address account, address[] calldata permissions, uint256 deadline, bytes calldata sig) external payable; function revokePermission(address account, address permission, uint256 deadline, bytes calldata sig) external; function revokePermissions(address account, address[] calldata permissions, uint256 deadline, bytes calldata sig) external; function replacePermission(address account, address oldPermission, address newPermission, uint256 deadline, bytes calldata sig) external payable; function replacePermissions(address account, address[] calldata oldPermissions, address[] calldata newPermissions, uint256 deadline, bytes calldata sig) external payable; function revokeSession(address account, uint256 deadline, bytes calldata sig) external; function activateSession(address account, uint256 deadline, bytes calldata sig) external; function setFeePolicy(address account, address newFeePolicy, address feeAsset, uint256 deadline, bytes calldata sig) external; // Dispatch function dispatch(address account, address permission, address target, uint256 value, bytes calldata data, bytes calldata managerSig, uint256 deadline) external; function dispatchBatch(address account, address permission, Call[] calldata calls, bytes calldata managerSig, uint256 deadline) external; // Fees & views function collectFees(address account, uint256 grossFee, uint256 currentNav, address feeToken) external; function getPermissions(address account) external view returns (address[] memory); function isPermissionRegistered(address account, address permission) external view returns (bool); function getPermissionsWithInfo(address account) external view returns (PermissionInfo[] memory); function previewBatch(address account, address permission, Call[] calldata calls) external view returns (bool approved, string memory reason); function hashTypedDataV4(bytes32 structHash) external view returns (bytes32); ``` See the [contract reference](/protocol/reference/contracts) for the full surface and the [EIP-712 reference](/protocol/reference/eip712) for every type hash. ## Signature verification [Section titled “Signature verification”](#signature-verification) Both manager and permission-signer signatures are verified with `_recoverOrERC1271`: ECDSA recovery is tried first (so EIP-7702 accounts that install transient code but don’t implement ERC-1271 still work), falling back to ERC-1271 `isValidSignature` when the signer is a contract. Both roles can therefore be EOAs, multisigs, or smart accounts. ## Module-execution boundary [Section titled “Module-execution boundary”](#module-execution-boundary) The kernel moves assets only through `ISafe.execTransactionFromModule(target, value, data, 0)` — always operation `0` (CALL), never DELEGATECALL. Two structural guards apply on dispatch: * **No self-targeting the Safe.** A call whose `target == account` reverts (`AccountSelfTarget`) — this blocks module-triggered `enableModule` / `setGuard` / owner changes that would satisfy the Safe’s `onlySelf` guard. * **No self-targeting the kernel** (batch only) — a subcall targeting the kernel reverts (`KernelSelfTarget`). Continue to [SailGovernance](/protocol/architecture/governance) or the [dispatch internals](/protocol/architecture/dispatch). # MandateFactory `MandateFactory` is a UX orchestrator. It bundles the two steps of attaching a configurable permission — `template.configure(...)` then `kernel.registerPermission(...)` — into a single transaction, and forwards the registration fee with an automatic refund of any excess. License: GPL-2.0-or-later. ## It holds no trust [Section titled “It holds no trust”](#it-holds-no-trust) The factory has **no privilege**. Every inner call is independently signature-authenticated: * `configure` carries an EIP-712 signature from the account’s Permission Signer (verified by the template against the kernel). * `registerPermission` carries an EIP-712 signature from the Permission Signer (verified by the kernel). So the factory’s only value is **bundling, fee forwarding, and a canonical entry point** for tooling. Anyone can deploy a template implementing `IConfigurablePermission` and use it with the factory immediately — no allowlist, no registry. Its `receive()` accepts ETH **only from the kernel** (the fee refund), preventing balance-inflation games against the refund accounting. ## Operations [Section titled “Operations”](#operations) | Function | What it does | | ------------------------ | ------------------------------------------------------------------------------------------ | | `attach` | `configure` one template, then `registerPermission`, one tx; refunds fee excess. | | `attachBatch` | `configure` N templates, then `registerPermissions` atomically. | | `reconfigure` | re-`configure` a template’s params for an account (no kernel touch). | | `replace` | `configure` a new template, then `replacePermission` (atomic kernel swap). | | `deployAndAttach` | clone a standalone template (EIP-1167), initialize it, then `registerPermission` — one tx. | | `detach` / `detachBatch` | `revokePermission(s)` for an account. | ## Deterministic clones [Section titled “Deterministic clones”](#deterministic-clones) `deployAndAttach` deploys an EIP-1167 minimal-proxy clone of a logic contract via `Clones.cloneDeterministic`, with the salt **namespaced by `msg.sender`** (`keccak256(abi.encode(msg.sender, salt))`) to prevent cross-caller salt squatting. Predict the address beforehand with `predictCloneAddress(impl, salt)` — **from the same EOA** that will send `deployAndAttach`, because of the namespacing. After init, a liveness check (`CloneInitializable.initialized()`) guards against a clone that failed to initialize. ## Events [Section titled “Events”](#events) `Attached`, `BatchAttached`, `Reconfigured`, `Replaced`, `Detached`, `BatchDetached`, and `CloneDeployedAndAttached(account, impl, clone, salt)`. Note Using the factory is optional. You can always call the kernel’s `registerPermission` / `replacePermission` / `revokePermission` directly — the factory just saves a transaction when a template also needs configuring. # SafeModuleEnabler `SafeModuleEnabler` is a tiny, stateless helper whose only job is to enable the Sail kernel as a Safe **module** on a freshly created Safe, in the same transaction that creates it. License: MIT. ## Why it’s needed [Section titled “Why it’s needed”](#why-its-needed) Safe’s `enableModule(address)` is gated by an `authorized` modifier requiring `msg.sender == address(this)` — i.e. the Safe can only enable a module via a call from *itself*. For a brand-new Safe that has not yet executed any owner transaction, the **only** window to satisfy that is the `to` / `data` **delegatecall** hook inside `Safe.setup`. The enabler exploits exactly that window: ```solidity contract SafeModuleEnabler { function enable(address module) external { ISafeModuleEnable(address(this)).enableModule(module); } } ``` When `Safe.setup` **delegatecalls** `enable(module)`, `address(this)` resolves to the **Safe itself**, so the inner `enableModule(module)` is a valid Safe → Safe call. The kernel becomes a module before the first owner transaction ever runs. ## Properties [Section titled “Properties”](#properties) * **Stateless and reentrancy-safe by construction** — it holds no storage. * **Must be delegatecalled.** Called directly, the inner `enableModule` targets the enabler (which has no such function) and reverts — so it cannot be misused as a standalone call. * **Deployed once per chain** at a chain-identical address, and referenced as the `to` target in the Safe initializer passed to `SailKernel.createAccount`. ## How the kernel uses it [Section titled “How the kernel uses it”](#how-the-kernel-uses-it) `createAccount` validates that the `to` target embedded in the Safe initializer (`safeInitializer[68:100]`) is on governance’s `trustedModuleSetup` allowlist — so only this audited enabler can be the setup delegatecall target. After deployment, `createAccount` checks `ISafe(account).isModuleEnabled(address(this))` and reverts with `ModuleNotEnabled` if the enabler did not run. This is the structural defense (Octane #1) against an attacker supplying a malicious setup delegatecall that overwrites the proxy’s storage. # Concepts The ideas you need before building on Sail, in reading order: * [Separately Managed Accounts](/protocol/concepts/smas) — what an onchain SMA is and why it exists. * [The three roles](/protocol/concepts/roles) — Owner, Permission Signer, Manager, and who holds what authority. * [The mandate & selective dispatch](/protocol/concepts/mandate) — the mandate is a set of contracts, and each dispatch names one. * [The four evaluation guarantees](/protocol/concepts/evaluation-guarantees) — static evaluation, gas isolation, selective authorization, fail-closed. * [Deterministic deployment](/protocol/concepts/deterministic-deployment) — why the core and every SMA have the same address on every chain. * [Glossary](/protocol/concepts/glossary) — precise definitions of every term. The terminology hierarchy, from largest to smallest: > **SMA** (a Safe account registered with the kernel) → **Mandate** (the set of permissions registered for that SMA) → **Permission** (one `IPermission` contract that authorizes a class of calls) → **Template** (a reusable permission implementation that many accounts configure independently). # Deterministic deployment Sail’s trusted core has the **same address on every supported chain**, and every SMA derives the **same address on every chain**. This is a deliberate property, not a coincidence, and it shapes how integrations are built. ## The core: identical addresses everywhere [Section titled “The core: identical addresses everywhere”](#the-core-identical-addresses-everywhere) Every core contract is deployed through the standard CREATE2 factory (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) with a **global, chain-independent salt per contract** and **identical constructor arguments** on every chain. A CREATE2 address is: ```plaintext address = keccak256(0xff ++ factory ++ salt ++ keccak256(initCode))[12:] ``` The factory is the same everywhere and the salt carries no chain ID, so the address is identical **iff the init code (which includes the constructor arguments) is identical**. The salts are: | Contract | Salt | | ------------------ | ------------------------------------- | | TimelockController | `keccak256("sail.timelock.v1")` | | SailGovernance | `keccak256("sail.governance.v1")` | | SailKernel | `keccak256("sail.kernel.v1")` | | MandateFactory | `keccak256("sail.mandatefactory.v1")` | | StandardFeePolicy | `keccak256("sail.feepolicy.v1")` | | SafeModuleEnabler | `keccak256("sail.modulenabler.v1")` | A key enabler is that `TimelockController` is **deployed separately and injected** into `SailGovernance`’s constructor, rather than constructed inline. That makes every `SailGovernance` constructor argument chain-independent — which is what lets its address (and therefore the kernel’s, and therefore every SMA’s) match across chains. See the [addresses reference](/protocol/reference/addresses) for the live values. ## Accounts inherit the property [Section titled “Accounts inherit the property”](#accounts-inherit-the-property) When an account is created, the kernel derives the Safe’s CREATE2 salt by binding the caller’s salt nonce together with the account’s principals: ```plaintext boundSalt = keccak256(saltNonce, caller, permissionSigner, manager, feePolicy) ``` The Safe initializer references only chain-identical contracts (the kernel and the module enabler), so: > The same owner, permission signer, manager, fee policy, and salt nonce produce the **same SMA address on every supported chain**. Assets sent to that address on any supported chain reach the same account — whether or not the Safe has been deployed there yet. ## Why bind principals into the salt? [Section titled “Why bind principals into the salt?”](#why-bind-principals-into-the-salt) Binding the principals into the salt also closes a front-running gap: a counterfactual address **cannot** be squatted with different principals. A deployment that supplies a different manager or signer lands at a *different* address, so an attacker cannot register “your” address with their own manager. (This corresponds to Octane findings #4 / #16.) ## Practical consequences [Section titled “Practical consequences”](#practical-consequences) * **Predict before you deploy.** You can compute an SMA’s address off-chain and fund it before the Safe exists on a given chain. * **One identity, many chains.** Cross-chain tooling can treat an SMA as a single address. * **Idempotent creation.** If a proxy already exists at the predicted address (a retry, or a legitimate pre-deploy), `createAccount` adopts it instead of reverting. See [deterministic addresses](/protocol/reference/deterministic-addresses) for the exact prediction formula and code. # The four evaluation guarantees Permissions are arbitrary, user-deployed Solidity. The kernel does not trust them — it *contains* them. Every permission evaluation, on every dispatch, is bounded by four structural guarantees. Together they mean the worst a buggy or malicious permission can do is **deny** calls, never widen authority or harm the kernel. ## 1. Static evaluation [Section titled “1. Static evaluation”](#1-static-evaluation) Permissions are invoked via `staticcall`: ```solidity (bool success, bytes memory ret) = permission.staticcall{gas: PERMISSION_GAS_CAP}( abi.encodeCall(IPermission.evaluate, (data, ctx)) ); ``` `staticcall` prohibits state mutation. A permission **cannot** modify any contract’s storage during evaluation, and **reentrancy through the permission surface is structurally impossible** — there is no write path to re-enter. **Protects against:** reentrancy, permissions that try to mutate state mid-evaluation, hidden side effects. ## 2. Gas isolation [Section titled “2. Gas isolation”](#2-gas-isolation) Each permission is called with a fixed gas cap, `PERMISSION_GAS_CAP = 150_000`. A permission that exceeds its cap reverts and is treated as returning `false`. (Batch permissions get a larger `BATCH_EVAL_GAS_CAP = 1_000_000` because they must inspect every subcall.) **Protects against:** a pathological permission denying service to the kernel or burning the manager’s entire gas budget. The cap bounds the cost of evaluation no matter what the permission does. ## 3. Selective authorization [Section titled “3. Selective authorization”](#3-selective-authorization) The manager’s signature names **one** registered permission. The kernel evaluates that permission alone; no other registered permission is consulted. The named permission must be registered on the account — registration is the Permission Signer’s trust anchor. **Protects against:** unrelated permissions falsely denying each other, and lets one account hold many independent mandates. See [the mandate](/protocol/concepts/mandate). ## 4. Fail-closed [Section titled “4. Fail-closed”](#4-fail-closed) Any permission that returns `false`, reverts, runs out of gas, or returns malformed data causes the **entire dispatch to revert**. There is no partial-allow path, and an account with no registered permissions cannot dispatch at all (deny-by-default). The kernel decodes the return as a `uint256` and requires it to equal `1`, so a non-canonical boolean word is treated as denial, not acceptance. **Protects against:** a buggy permission accidentally allowing a call. The default behavior of anything that goes wrong is to deny. *** These four properties protect the kernel *from* the permission. Everything a permission expresses **inside** that envelope — which tokens, which venues, what amounts — is the permission author’s responsibility. That division is what makes the kernel auditable in isolation and the permission set permissionlessly extensible. See [full expressiveness](/protocol/permissions/expressiveness). # Glossary Precise definitions of the terms used throughout these docs. Where a term maps to a code identifier, that is noted. **SMA (Separately Managed Account)** — A [Safe](https://safe.global) smart account registered with `SailKernel`, with the kernel enabled as a Safe module. Holds the owner’s capital self-custodially. **Owner** — The party that holds the Safe and custodies the SMA’s capital, via the Safe’s own owners and threshold. Can always revoke the manager. **Permission Signer** — The address whose EIP-712 signatures authorize permission-registry operations (register, configure, replace, revoke, session, fee-policy). `AccountConfig.permissionSigner`. Cannot move assets. **Manager** — The address whose EIP-712 signatures authorize dispatches. `AccountConfig.manager`. May be an EOA, multisig, MPC wallet, or autonomous agent. Cannot change the mandate. **Submitter** — The `msg.sender` of a dispatch; the address that pays gas. Not an authority role; may be a relayer. Exposed to permissions as `ctx.submitter`. **Mandate** — The set of permission contracts registered for an SMA. Defines what the manager is authorized to do. Not a document; a set of contracts. **Permission** — A contract implementing `IPermission` that decides whether a given call is allowed, via `evaluate(txData, ctx) → bool`. **Template** — A reusable permission *implementation*. Shared (multi-tenant) templates serve many accounts from one deployment, each configured independently; standalone templates are one instance per account. **Dispatch** — A manager-signed request to execute one call through the SMA, naming one registered permission as its authorizer. `SailKernel.dispatch(...)`. **Batch dispatch** — A manager-signed request to execute an ordered array of calls atomically, gated by one batch-aware permission (`IBatchPermission`). `SailKernel.dispatchBatch(...)`. **Selective authorization** — The model in which the manager’s signature names one registered permission and only that permission is evaluated. **Fail-closed** — The property that any permission failure (false, revert, OOG, malformed return) reverts the whole dispatch. Deny-by-default. **`Context` / `ctx`** — The read-only snapshot of the dispatch environment passed to a permission’s `evaluate` (account, manager, submitter, target, selector, value, blockTimestamp, blockNumber). **Kernel** — `SailKernel`, the single trusted execution contract. Registers accounts, holds the permission registry, verifies signatures, dispatches to the Safe, accounts for fees. **Fee policy** — A contract implementing `IFeePolicy` that computes the maximum collectable fee for an account and records collection state. The reference implementation is `StandardFeePolicy`. **Protocol cut** — The protocol’s share of a manager-collected fee, in basis points, bounded by the immutable cap `MAX_PROTOCOL_CUT_BPS = 2500` (25%). Zero at launch. **Registration fee** — A flat native-token fee charged per permission registration, bounded by the immutable cap `MAX_PERMISSION_FEE_WEI` (≤ `0.01` native). Live at launch (`0.00015` native; higher on BSC and HyperEVM). **High-water mark (HWM)** — The highest NAV seen at collection time for an account; `StandardFeePolicy` charges performance fees only on gains above it. **NAV** — Net asset value, supplied by the manager at fee collection. Not independently verified by the kernel; the fee policy is the guard against inflated NAV. **Timelock** — The standalone `TimelockController` (48-hour delay) through which all governance parameter changes flow. **`PERMISSION_GAS_CAP`** — The fixed gas budget (`150_000`) for each permission’s `evaluate` staticcall. `BATCH_EVAL_GAS_CAP` (`1_000_000`) is the batch equivalent. **`boundSalt`** — `keccak256(saltNonce, caller, permissionSigner, manager, feePolicy)`, the salt that binds an SMA’s CREATE2 address to its principals. # The mandate & selective dispatch ## The mandate is a set of contracts, not a document [Section titled “The mandate is a set of contracts, not a document”](#the-mandate-is-a-set-of-contracts-not-a-document) In Sail, the **mandate** is the set of permissions registered for an SMA — contracts implementing `IPermission` that define what the manager is authorized to do. The Safe is the account the mandate applies to; it is not itself part of the mandate. The kernel stores, per account, an ordered list of permission addresses plus an index map for O(1) membership checks. Adding to the mandate is `registerPermission` (or `registerPermissions` for several at once); removing is `revokePermission` / `revokePermissions`; swapping atomically is `replacePermission` / `replacePermissions`. Each of these is authorized by an EIP-712 signature from the **Permission Signer**. ## Selective dispatch: one named permission authorizes each call [Section titled “Selective dispatch: one named permission authorizes each call”](#selective-dispatch-one-named-permission-authorizes-each-call) When the manager submits a transaction, its signature **names one** registered permission as the authorizer for that dispatch: ```solidity function dispatch( address account, address permission, // the single permission that must approve this call address target, uint256 value, bytes calldata data, bytes calldata managerSig, uint256 deadline ) external; ``` The kernel calls `evaluate()` on **that permission alone** — no other registered permission is consulted — and dispatches the call to the Safe only if it returns `true`. This is **selective authorization**. It replaced an earlier conjunctive (AND-of-all) model, and the difference matters: with selective dispatch, unrelated permissions can coexist on one account without falsely denying each other. A swap permission, a borrow permission, and a transfer permission can all be registered; each call names the one that should authorize it, and the others stay out of the way. Note **Mandate = union; dispatch = selection.** The mandate is the *union* of all registered permissions. Each dispatch *selects* one of them as its authorizer. Layered “must satisfy several permissions at once” composition is not provided by the kernel — express compound rules inside a single permission (or a batch permission) instead. ## What happens on a denied call [Section titled “What happens on a denied call”](#what-happens-on-a-denied-call) If the manager names a permission that is not registered, the kernel reverts with `PermissionNotRegistered` before any evaluation. If the named permission returns `false`, reverts, runs out of gas, or returns malformed data, the kernel reverts the whole dispatch with `PermissionDenied`. Either way, **no state change occurs** — the call never reaches the Safe. ## Batches [Section titled “Batches”](#batches) For multi-step strategies that must execute atomically (the classic case being an `approve → call → reset` sequence), the kernel exposes `dispatchBatch`, gated by a single **batch-aware** permission implementing `IBatchPermission`. The same selective model applies: one named permission validates the entire call sequence. See [single & batch dispatch](/protocol/architecture/dispatch). Next: the [four guarantees](/protocol/concepts/evaluation-guarantees) that protect the kernel from whatever a permission does. # The three roles Sail separates three roles that exist implicitly in any managed-account structure. Keeping them distinct is what lets an agent transact without ever being able to take custody or rewrite its own mandate. | Role | Authority | Held by | Code identifier | | --------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | -------------------------------- | | **Owner** | Holds the Safe. Custodies the SMA’s capital. Always self-custodial. Can always revoke the manager. | The LP, who owns the Safe. | the Safe’s own owners/threshold | | **Permission Signer** | Authorizes the mandate. Signs registration, configuration, replacement, and revocation of permissions via EIP-712. | The Owner, or a separate signing key or multisig. | `AccountConfig.permissionSigner` | | **Manager** | Executes transactions within bounds. Cannot exceed what the registered permissions allow. | EOA, multisig, MPC wallet, or autonomous agent. | `AccountConfig.manager` | These three are stored per account in the kernel’s `AccountConfig` (alongside the `feePolicy`, the canonical `feeAsset`, and the `sessionActive` flag). ## What each role can and cannot do [Section titled “What each role can and cannot do”](#what-each-role-can-and-cannot-do) * The **Owner** controls the Safe and its assets directly, through the Safe’s own threshold. The kernel reads custody from the Safe; it never holds funds. The Owner can rotate the manager (see below) and can remove the kernel module entirely. * The **Permission Signer** decides *what the manager may do* by signing permission-registry operations. It **cannot move assets** — it never signs a dispatch. * The **Manager** decides *which authorized action to take, and when*, by signing dispatches. It **cannot register or revoke permissions**, and it **cannot exceed** any registered permission’s bounds. This separation is a security property: a compromised manager key cannot widen its own mandate, and a compromised permission-signer key cannot move funds. ## The submitter is not a role [Section titled “The submitter is not a role”](#the-submitter-is-not-a-role) The address that pays gas and submits the manager’s signed dispatch is **not** an authority. Any address may submit a dispatch; authority derives entirely from the manager’s signature, the registered permissions, and the kernel’s evaluation. This makes Sail natively compatible with relayers, paymasters, and ERC-4337 bundlers. The permission can still read `ctx.submitter` and gate on it if it chooses. ## Retail vs. institutional setups [Section titled “Retail vs. institutional setups”](#retail-vs-institutional-setups) * **Retail:** all three roles collapse to one key or wallet. The owner deploys a Safe, registers permissions as Permission Signer, and signs dispatches as Manager. * **Institutional:** the roles separate. A fund firm holds the Manager key; an independent compliance officer or the client holds the Permission Signer key and controls what the manager can trade; the Safe signers (Owner) retain custody and can always revoke. ## Manager rotation [Section titled “Manager rotation”](#manager-rotation) The Safe itself (`msg.sender == account`) can rotate the manager via `setManager(newManager)`. Rotation **clears every registered permission atomically** — no mandate silently transfers authority approved for the old manager to a new one — and bumps the manager nonce epoch so any dispatch the old manager pre-signed is invalidated. The owner re-approves each mandate for the new manager through the normal registration flow. Next: how those permissions form a [mandate](/protocol/concepts/mandate). # Separately Managed Accounts A **separately managed account (SMA)** is the structure professional asset management is built on. Capital is held in an account titled to the owner; a designated manager executes within bounds set by a mandate. Unlike a pooled fund: * assets remain **separately custodied** for each owner; * the manager has **bounded authority**, not full discretion; * the mandate can be **revised, narrowed, or revoked** at any time; * performance is **attributed per account**. Off-chain, these properties have kept the SMA an institutional product — the cost of custodians, legal infrastructure, and reconciliation is prohibitive below account minimums of $100k–$1M. ## The onchain SMA [Section titled “The onchain SMA”](#the-onchain-sma) Sail expresses the SMA relationship as a minimal on-chain primitive: * **Custody** is a self-custodial [Safe](https://safe.global) owned by the LP. The Sail kernel is enabled as a Safe **module** — it can ask the Safe to execute calls, but only through the gated dispatch path, and it can never move assets outside that path. * **The manager** holds no assets. It holds a key whose signature authorizes individual transactions, each of which the kernel checks against the account’s registered permissions before the Safe executes it. * **The mandate** is a set of on-chain permission contracts. Revising it is a signed on-chain operation; revoking the manager takes effect in a single block. Because the manager is a key, not a person, it can be an **autonomous agent**. And because the mandate is enforced by the kernel on every call, the agent’s authority is bounded by code, not by trust. ## Why it matters for agents [Section titled “Why it matters for agents”](#why-it-matters-for-agents) A traditional mandate is static — one template applied uniformly across a book of accounts. An agent operating inside an onchain SMA can run a **distinct strategy per account**, calibrated to each owner’s balance, risk tolerance, and horizon, while sharing the same infrastructure, permission templates, and fee model. The marginal cost of one more account is one more agent execution. ## What the kernel is *not* [Section titled “What the kernel is not”](#what-the-kernel-is-not) * It is **not a custodian.** Assets never leave the Safe except through a dispatch that satisfies a registered permission. * It is **not a strategy engine.** It knows nothing about DeFi venues; it evaluates a permission contract and respects the answer. * It is **not an upgrade surface for venues.** New venues are new permission contracts, deployed permissionlessly. Next: [the three roles](/protocol/concepts/roles) that make this work. # Fees & governance Two short pages: * [Fee model](/protocol/fees-and-governance/fees) — the two fee mechanisms, the immutable caps, and the reference `StandardFeePolicy`. * [Governance](/protocol/fees-and-governance/governance) — the 48-hour timelock, emergency pause, two-step transfer, and the trusted allowlists. In one sentence: **fees are bounded by immutable constitutional caps and are zero at launch; all parameter changes flow through a 48-hour timelock; an emergency admin can pause the kernel for at most 72 hours.** # Fee model The protocol has exactly two fee mechanisms, each bounded by an **immutable constitutional cap**. They differ in scope: * **Fee 1** is **universal** — a flat charge on every permission registration, so it touches every SMA. It is **live** at a small non-zero rate (see below). * **Fee 2** is **conditional** — a protocol cut taken only when a manager actually collects a management/performance fee from the SMAs it runs, and is **zero at launch**. An SMA that never has a fee charged against it never incurs Fee 2. Strategy-level fees (the management/performance schedule the manager charges) are a separate, optional layer that lives in the fee policy; Fee 2 is the protocol’s slice of those. ## Fee 1 — Permission registration fee [Section titled “Fee 1 — Permission registration fee”](#fee-1--permission-registration-fee) A flat native-token fee charged each time a permission is registered, paid to the protocol treasury: ```plaintext total fee = permissionRegistrationFee × n_permissions ``` * Bounded by the immutable cap `MAX_PERMISSION_FEE_WEI`, an immutable bytecode ceiling of **0.01** in the chain’s native unit, fixed in the `SailGovernance` constructor. * The active rate (`permissionRegistrationFee`) is governance-tunable within that cap, via the 48-hour timelock. * **Live at launch.** The current rate is **`0.00015 ETH`** on the 10 ETH-native chains, **`0.00045 BNB`** on BSC, and **`0.005 HYPE`** on HyperEVM. It deployed at `0.00015` native on every chain (CREATE2 requires byte-identical constructor args); governance later raised the live rate on BSC and HyperEVM through the timelock, which does not change the already-locked contract address. Excess `msg.value` is refunded to the submitter. * Denominated in each chain’s native token, with no oracle dependency; governance retunes per chain as native-token prices move. See [Deployment addresses → Fees](/protocol/reference/addresses#fees-live) for the current per-chain rates. ## Fee 2 — Protocol cut on manager-collected fees [Section titled “Fee 2 — Protocol cut on manager-collected fees”](#fee-2--protocol-cut-on-manager-collected-fees) Unlike Fee 1, **Fee 2 is not universal.** It is a cut the protocol takes *off the top* of a fee that a **manager** is already charging an SMA — so it only exists where a manager actually crystallises a strategy fee. The protocol earns nothing here unless a manager earns first. ### When Fee 2 applies — and when it doesn’t [Section titled “When Fee 2 applies — and when it doesn’t”](#when-fee-2-applies--and-when-it-doesnt) Fee 2 is reached **only** through `collectFees`, and `collectFees` only succeeds for an account that has a fee policy set (`setFeePolicy` → `IFeePolicy`). The intended shape is a **yield or portfolio-management strategy**: a single manager wallet is the delegated signer across **one or more SMAs**, runs the strategy on each, and periodically collects a management and/or performance fee from each account it manages. Fee 2 is the protocol’s slice of each of those collections. Concretely, Fee 2 **does** apply when: * A manager (EOA, multisig, bot, or AI agent) is delegated over a set of SMAs whose owners have agreed to pay it a management/performance fee, and * the SMAs have a `trustedFeePolicy` set that names that manager as `feeRecipient`, and * the manager calls `collectFees` to crystallise its fee on a given account. Fee 2 **does not** apply to: * An SMA with **no fee policy** set (`collectFees` reverts with `FeePolicyNotSet`) — e.g. a retail owner self-managing, or any account whose manager simply isn’t charging a fee. Such an account can register permissions, dispatch, and operate forever and **never** touch Fee 2. * Mere registration, dispatch, or any activity other than a manager fee collection. Fee 1 covers registration; Fee 2 is strictly about manager fee crystallisation. So the two fees scale on different axes: **Fee 1 scales with how many permissions get registered (every SMA), Fee 2 scales with how much fee revenue a manager generates across the SMAs it manages.** ### The shared-policy, multi-SMA case [Section titled “The shared-policy, multi-SMA case”](#the-shared-policy-multi-sma-case) A fee policy is a standalone contract on governance’s `trustedFeePolicy` allowlist, and **one policy instance can be the registered policy for many SMAs at once**. In the typical strategy setup, the manager deploys (or reuses) a single `StandardFeePolicy` whose `feeManager`/`feeRecipient` is the manager wallet, and every SMA that opts into that strategy points its `feePolicy` at that one contract. State that must be per-account — high-water mark, last-collection timestamp, applied rates — is keyed by `account` inside the policy, so each SMA accrues independently even though they share the schedule. When the manager later collects from each of those SMAs, the kernel takes the same `currentProtocolCutBps` slice from each collection. The protocol cut is computed **per collection, per account** — there is no aggregation across the manager’s book. ### The split [Section titled “The split”](#the-split) When `collectFees` runs, the kernel splits the gross amount the manager requested: ```plaintext protocolCut = grossFee × currentProtocolCutBps / 10_000 remainder = grossFee − protocolCut distributorCut = remainder × distributorBps / 10_000 (0 if distributor == address(0)) managerTake = remainder − distributorCut ``` * `currentProtocolCutBps` is bounded by the immutable cap `MAX_PROTOCOL_CUT_BPS = 2500` (**25%**) and is **zero at launch** — at launch the manager (and its distributor) keep 100% of the fee they charge. * `grossFee` is capped by the policy: the kernel enforces `grossFee <= computeFee(...).grossFee`, so the manager can never collect more than its own schedule allows. * The fee **recipient is pulled from the policy** (`IFeePolicy.feeRecipient()`), not from the caller — a compromised manager cannot redirect fees to itself. * `distributorBps` is validated `<= 10_000` (`DistributorBpsTooLarge` otherwise). The distributor share is carved out of the manager’s *remainder*, not the protocol’s cut. * `collectFees(account, grossFee, currentNav, feeToken)` may be called by the manager, the Safe itself, or the permission signer (a backstop against fee-starvation). `feeToken` must equal the account’s configured `feeAsset`. State is recorded **before** transfers (CEI); transfers go out of the Safe via the module path, atomically. ## IFeePolicy [Section titled “IFeePolicy”](#ifeepolicy) The kernel delegates fee *computation* to a policy contract on governance’s `trustedFeePolicy` allowlist: ```solidity interface IFeePolicy { function feeRecipient() external view returns (address); function computeFee(address account, uint256 currentNav) external view returns (uint256 grossFee, address distributor, uint256 distributorBps); function recordCollection(address account, uint256 grossFee, uint256 currentNav) external; // onlyKernel } ``` The kernel enforces `grossFee <= computeFee(...).grossFee` — the policy sets the ceiling; the kernel enforces it. ## StandardFeePolicy — the reference 2-and-20 [Section titled “StandardFeePolicy — the reference 2-and-20”](#standardfeepolicy--the-reference-2-and-20) `StandardFeePolicy` implements a classic schedule: ```plaintext managementFee = currentNav × managementFeeBps × elapsed / (365 days × 10_000) performanceFee = max(currentNav − HWM, 0) × performanceFeeBps / 10_000 grossFee = managementFee + performanceFee ``` * After each collection the **high-water mark** updates to `max(HWM, currentNav)` — performance fees are charged only on new all-time highs. * Rate caps: management `<= 1000` bps (10%/yr), performance `<= 5000` bps (50%), distributor `<= 10000` bps. * A **minimum 1-day collection interval** prevents management-fee timer manipulation via rapid calls. * The HWM must be **explicitly seeded** by the fee manager (`seedHighWaterMark`) before any collection — this blocks a manager from seeding HWM at 0 and claiming a performance fee on the whole portfolio on the first call. * Rate changes are **prospective**: each account snapshots the applied rates at collection time, so a rate change never reprices a past period. * `feeRecipient()` returns the `feeManager`. Fee-manager control transfers two-step (`proposeFeeManager` → `acceptFeeManager`). Note **NAV is manager-attested.** `currentNav` is supplied by the manager and is **not** independently verified on-chain. `StandardFeePolicy` trusts it; strategies that need trustless fee computation should use an oracle-backed policy instead. See [Security → limitations](/protocol/security/limitations). A custom policy need only implement `IFeePolicy`, be added to the `trustedFeePolicy` allowlist by governance, and (if it needs validated NAV) read an oracle inside `computeFee` rather than trusting the manager’s value. # Governance Sail governance is deliberately small: a fixed set of tunable parameters within immutable caps, all changes behind a 48-hour timelock, plus a narrow emergency pause. The full mechanics live in [Architecture → SailGovernance](/protocol/architecture/governance); this page is the operator-facing summary. ## What governance can change (and the caps it can’t exceed) [Section titled “What governance can change (and the caps it can’t exceed)”](#what-governance-can-change-and-the-caps-it-cant-exceed) | Parameter | Setter (timelock) | Default | Hard cap (immutable) | | ---------------------------------------------------------------------------------------- | ------------------------------ | ----------------------------------------------- | -------------------------------------- | | Protocol cut | `setProtocolCutBps` | 0 | `MAX_PROTOCOL_CUT_BPS = 2500` (25%) | | Registration fee | `setPermissionRegistrationFee` | live (`0.00015` native; higher on BSC/HyperEVM) | `MAX_PERMISSION_FEE_WEI ≤ 0.01` native | | Permissions per account | `setMaxPermissionsPerAccount` | 20 | `MAX_PERMISSIONS_CAP = 100` | | Trusted Safe factory / singleton / module-setup / proxy-codehash / fee-policy allowlists | `setTrusted*` | seeded at genesis | — | | Treasury (on the kernel) | `setTreasury` | set at deploy | — | | Emergency admin | `rotateEmergencyAdmin` | set at deploy | — | No governance action can raise a constitutional cap. Every setter above is `onlyTimelock`: **schedule → wait 48 hours → execute**. ## The 48-hour timelock [Section titled “The 48-hour timelock”](#the-48-hour-timelock) All parameter changes flow through a standalone OpenZeppelin `TimelockController` with a minimum delay of **exactly** 48 hours. It is deployed separately and injected into `SailGovernance`, whose constructor validates the delay, the proposer/executor roles, and self-administration (see [Architecture → SailGovernance](/protocol/architecture/governance)). Genesis allowlist seeding via `bootstrapAllowlists` is the one timelock bypass, usable exactly once, then permanently latched off. ## Two-step governance transfer [Section titled “Two-step governance transfer”](#two-step-governance-transfer) `proposeGovernance → acceptGovernance`, with a mandatory `rotateTimelockRoles` in between so the incoming governance holds the timelock’s proposer/executor/canceller roles before it accepts — there is no window where the old governance keeps timelock keys after handoff. ## Emergency pause [Section titled “Emergency pause”](#emergency-pause) A separate `emergencyAdmin` can `pause()` the kernel **without** a timelock delay, for fast incident response: * **Auto-expires after 72 hours.** No action needed to resume. * **72-hour cooldown** between pauses; `unpause()` lifts early. * While paused: `dispatch`, `dispatchBatch`, `collectFees`, and permission *registration* are blocked — but **revocation, session revocation, manager rotation, and clearing a fee policy stay available**, so owners can always reduce exposure during an incident. The emergency admin can pause but cannot change parameters or move funds; parameter authority remains with the timelock. # Guides Hands-on walkthroughs, each grounded in the real on-chain signatures. Follow them in order for an end-to-end path from nothing to a running, bounded agent — or jump to the one you need. 1. [Write your first permission](/protocol/guides/write-a-permission) — a minimal `IPermission`, line by line. 2. [Deploy an SMA](/protocol/guides/deploy-an-sma) — create a Safe with the kernel enabled, and derive its deterministic address. 3. [Register a mandate & appoint a manager](/protocol/guides/register-a-mandate) — sign and submit permission registration. 4. [Dispatch a transaction within bounds](/protocol/guides/dispatch) — execute a call, and see what a denial looks like. 5. [Use a shared template](/protocol/guides/use-a-template) — configure `SharedBoundedSwapPermission` end to end. Note These guides show the **protocol-level** mechanics (raw EIP-712 + contract calls). If you’d rather drive all of this from a TypeScript SDK and CLI — with signing, nonce handling, and deployment registries done for you — see the **Sailor** section. Sailor wraps exactly these flows. All examples target a kernel already deployed at the address in [Reference → Deployment addresses](/protocol/reference/addresses). EIP-712 domain: `name = "SailKernel"`, `version = "1"`, `verifyingContract =` the kernel, `chainId =` your chain. # Deploy an SMA An SMA is a Safe with the Sail kernel enabled as a module and registered in the kernel’s `configs`. There are two ways to get one. ## Option A — create a new Safe and register in one call [Section titled “Option A — create a new Safe and register in one call”](#option-a--create-a-new-safe-and-register-in-one-call) `createAccount` deploys a Safe through a trusted factory **and** registers it, atomically: ```solidity function createAccount( address safeFactory, // must be on governance.trustedSafeFactory address safeSingleton, // must be on governance.trustedSafeSingleton bytes calldata safeInitializer, // Safe.setup calldata; its delegatecall `to` must be the trusted SafeModuleEnabler uint256 saltNonce, // your nonce; bound with msg.sender + principals into the CREATE2 salt address permissionSigner, // authorizes the mandate address manager, // authorizes dispatches (your agent) address feePolicy, // address(0) for none; otherwise must be trusted address feeAsset // canonical fee token; address(0) = native ETH ) external returns (address account); ``` The `safeInitializer` is the Safe’s own `setup(...)` calldata, where the `to`/`data` delegatecall hook points at the **`SafeModuleEnabler`** so the kernel is enabled as a module during setup (see [SafeModuleEnabler](/protocol/architecture/safe-module-enabler)). The kernel: 1. checks `safeFactory` / `safeSingleton` are trusted; 2. extracts the setup delegatecall target from `safeInitializer[68:100]` and checks it’s a trusted module-setup helper (or `address(0)` for a vanilla setup); 3. computes the bound salt and predicts the proxy address — if a proxy already exists there, it **adopts** it instead of redeploying; 4. requires the deployed Safe to have the kernel enabled as a module (`ModuleNotEnabled` otherwise); 5. registers the account in `configs`. The bound salt is: ```plaintext boundSalt = keccak256(saltNonce, msg.sender, permissionSigner, manager, feePolicy) ``` ## Option B — register an existing Safe [Section titled “Option B — register an existing Safe”](#option-b--register-an-existing-safe) If you already have a Safe, enable the kernel as a module and have the Safe call `registerAccount` itself: ```solidity function registerAccount( address permissionSigner, address manager, address feePolicy, address feeAsset ) external; // MUST be called by the Safe (msg.sender == Safe) ``` The kernel verifies the caller’s runtime codehash is a trusted Safe-proxy codehash and that the kernel is already enabled as a module — so only a genuine Safe that has actually added the module can self-register. Typically the `enableModule` and `registerAccount` calls are batched in one Safe transaction. ## Predict the address first [Section titled “Predict the address first”](#predict-the-address-first) Because the salt binds the principals, you can compute the SMA’s address **before** deploying, and it is identical on every supported chain (see [deterministic addresses](/protocol/reference/deterministic-addresses)). This lets you fund the address ahead of deployment. Note the front-run protection: a deployment supplying a *different* `permissionSigner` or `manager` lands at a *different* address, so no one can squat your counterfactual SMA with their own principals. ## What you have now [Section titled “What you have now”](#what-you-have-now) A registered SMA with `sessionActive = true`, a `permissionSigner`, a `manager`, and (optionally) a fee policy — but **no permissions yet**. Until you register at least one permission, every dispatch reverts with `PermissionNotRegistered` (deny-by-default). Next: [register a mandate](/protocol/guides/register-a-mandate). # Dispatch a transaction within bounds With the SMA registered and at least one permission attached, the manager can execute calls. Each dispatch names the one permission that should authorize it. ## 1. Build the Dispatch signature [Section titled “1. Build the Dispatch signature”](#1-build-the-dispatch-signature) The selective-model type string (note the `permission` field): ```plaintext Dispatch(address account,address permission,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline) ``` Sign with the **manager** key: ```plaintext dataHash = keccak256(data) // the call's calldata structHash = keccak256(abi.encode( DISPATCH_TYPEHASH, account, permission, target, value, dataHash, managerNonces[account], // read from the kernel deadline )) digest = kernel.hashTypedDataV4(structHash) managerSig = sign(digest, managerKey) // ECDSA or ERC-1271 ``` Read the nonce from `kernel.managerNonces(account)`. ## 2. Submit [Section titled “2. Submit”](#2-submit) ```solidity kernel.dispatch(account, permission, target, value, data, managerSig, deadline); ``` The kernel verifies the signature, staticcalls `permission.evaluate(data, ctx)` under the 150k gas cap, and — only if it returns `true` — executes `execTransactionFromModule(target, value, data, 0)` on the Safe. On success it emits `Dispatched(account, permission, target, selector, value)`. ## What a denial looks like [Section titled “What a denial looks like”](#what-a-denial-looks-like) The dispatch **reverts** (no state change) in these cases: | Revert | Cause | | ------------------------------------- | ----------------------------------------------------------------------------------- | | `PermissionNotRegistered(permission)` | you named a permission that isn’t on the account’s mandate | | `PermissionDenied(permission)` | the permission returned false, reverted, ran out of gas, or returned malformed data | | `SessionInactive(account)` | the session is paused (`revokeSession` was called) | | `DeadlineExpired(deadline, now)` | `block.timestamp > deadline` | | `InvalidManagerSignature()` | the signature didn’t recover to the account’s `manager` | | `AccountSelfTarget()` | `target == account` (blocked — would let the Safe reconfigure itself) | | `SafeExecutionFailed()` | the permission approved it but the Safe call itself failed | | `ProtocolPaused()` | the protocol is paused | Because evaluation is fail-closed, a buggy permission denies rather than over-permits. If you hit `PermissionDenied`, re-check the permission’s bounds against your call — it’s doing its job. ## Nonces and in-flight invalidation [Section titled “Nonces and in-flight invalidation”](#nonces-and-in-flight-invalidation) Each successful dispatch increments `managerNonces[account]`. If the Permission Signer revokes a permission, revokes the session, replaces a permission, or the Safe rotates the manager, the kernel bumps the manager-nonce **epoch** (`1 << 128`), which invalidates **every dispatch the manager had pre-signed but not yet submitted**. This means tightening the mandate cannot be raced by a stale signature. ## Batches [Section titled “Batches”](#batches) For an atomic multi-call sequence (e.g. `approve → call → reset`), use `dispatchBatch` with a batch-aware permission. The signature is over `DispatchBatch(account, permission, callsHash, nonce, deadline)` with `callsHash = keccak256(abi.encode(calls))`, using the separate `batchNonces`. You can pre-check a batch off-chain with `previewBatch(account, permission, calls)`. See [single & batch dispatch](/protocol/architecture/dispatch). # Register a mandate & appoint a manager The manager was appointed when the account was created (the `manager` argument to `createAccount` / `registerAccount`). The mandate — what that manager may do — is set by **registering permissions**, each authorized by the Permission Signer’s EIP-712 signature. ## 1. Build the RegisterPermission signature [Section titled “1. Build the RegisterPermission signature”](#1-build-the-registerpermission-signature) The type string (note the `deadline` field — current kernels include it): ```plaintext RegisterPermission(address account,address permission,uint256 nonce,uint256 deadline) ``` Construct and sign the digest with the Permission Signer key: ```plaintext structHash = keccak256(abi.encode( REGISTER_PERMISSION_TYPEHASH, account, permission, signerNonces[account], // read from the kernel deadline )) digest = kernel.hashTypedDataV4(structHash) // applies the SailKernel domain signerSig = sign(digest, permissionSignerKey) // ECDSA or ERC-1271 ``` Read the current nonce from `kernel.signerNonces(account)`. The EIP-712 domain is `{ name: "SailKernel", version: "1", chainId, verifyingContract: kernel }`. ## 2. Submit the registration with the fee [Section titled “2. Submit the registration with the fee”](#2-submit-the-registration-with-the-fee) ```solidity uint256 fee = governance.permissionRegistrationFee(); // 0 at launch kernel.registerPermission{value: fee}(account, permission, deadline, signerSig); ``` Anyone may submit this transaction — authority is the signature, not the sender. Excess `msg.value` is refunded. The permission must be a deployed contract, not already registered, and the account must be below `maxPermissionsPerAccount` (default 20). ## Register several at once [Section titled “Register several at once”](#register-several-at-once) To attach multiple permissions under a single signer nonce, sign `RegisterPermissions(address account,address[] permissions,uint256 nonce,uint256 deadline)` (the `address[]` is hashed per EIP-712 §4 — `keccak256` of the ABI-encoded, zero-padded addresses) and call: ```solidity kernel.registerPermissions{value: fee * permissions.length}(account, permissions, deadline, signerSig); ``` ## If the permission needs configuring [Section titled “If the permission needs configuring”](#if-the-permission-needs-configuring) Shared templates must be **configured** for the account in addition to being registered. You can do both in one transaction with the `MandateFactory.attach(...)`, which calls `template.configure(...)` then `kernel.registerPermission(...)`. See [Use a shared template](/protocol/guides/use-a-template). ## Changing the mandate later [Section titled “Changing the mandate later”](#changing-the-mandate-later) * **Tighten safely:** prefer `replacePermission` (atomic swap to a freshly-configured permission) over reconfiguring in place. * **Narrow:** `revokePermission` / `revokePermissions`. * **Stop everything:** `revokeSession` (then `activateSession` to resume). * **Rotate the agent:** the Safe calls `setManager(newManager)` — this **clears the whole mandate** and you re-register for the new manager. Each of these is a signed operation that consumes a `signerNonces` value and (for restrictive ops) invalidates the manager’s outstanding pre-signed dispatches. See [the lifecycle](/protocol/permissions/lifecycle). Now the manager can act → [Dispatch a transaction within bounds](/protocol/guides/dispatch). # Use a shared template `SharedBoundedSwapPermission` is an **example** template that demonstrates the shared multi-tenant pattern — one deployed contract serving many accounts, each with its own configuration. This guide configures it for an account end to end; the pattern generalizes to every shared template, with only the `params` layout changing. As with all shipped templates, treat it as a reference to read, adapt, and verify rather than an audited, drop-in contract — see the framing on [Shared multi-tenant templates](/protocol/permissions/shared-templates). ## 1. Encode the config params [Section titled “1. Encode the config params”](#1-encode-the-config-params) `SharedBoundedSwapPermission` decodes this exact tuple: ```solidity bytes memory params = abi.encode( routers, // address[] allowlisted swap routers tokensIn, // address[] tokens the agent may sell tokensOut, // address[] tokens the agent may buy maxAmountPerTx, // uint256 per-swap input cap maxSlippageBps, // uint256 max slippage in bps (< 10000) priceOracle, // address IOracle; address(0) disables the oracle check maxPriceAgeSec // uint256 required freshness bound if priceOracle != 0 ); ``` Constraints enforced at configure time: `maxSlippageBps` must be `< 10000` (`SlippageBpsTooLarge` otherwise), and if `priceOracle != address(0)` then `maxPriceAgeSec` must be non-zero (`MissingPriceAge` otherwise — a configured oracle must come with a freshness bound). ## 2. Configure + register in one transaction [Section titled “2. Configure + register in one transaction”](#2-configure--register-in-one-transaction) Use the `MandateFactory.attach`, which calls `template.configure(...)` then `kernel.registerPermission(...)`: ```solidity factory.attach{value: registrationFee}( account, template, // the SharedBoundedSwapPermission deployment params, configureDeadline, configureSig, // Permission Signer EIP-712 over Configure(account, keccak256(params), nonce, deadline) kernelDeadline, kernelSig // Permission Signer EIP-712 over RegisterPermission(account, template, nonce, deadline) ); ``` The two signatures use two different nonces: `configureSig` uses the template’s `configNonces(account)`; `kernelSig` uses the kernel’s `signerNonces(account)`. The factory holds no privilege — both inner calls are authorized purely by these signatures — and refunds any fee excess. The `Configure` type string (from `BaseSharedPermission`): ```plaintext Configure(address account,bytes32 paramsHash,uint256 nonce,uint256 deadline) ``` ## 3. Dispatch a swap [Section titled “3. Dispatch a swap”](#3-dispatch-a-swap) The manager now dispatches a swap call, naming the template as the permission. `SharedBoundedSwapPermission.evaluate` recognizes three selectors and checks, per the account’s config: * Uniswap V3 `exactInputSingle` (`0x414bf389`, with deadline) and SwapRouter02 variant (`0x04e45aaf`, no deadline); * V2-style `swapExactTokensForTokens` (`0x38ed1739`). For each, it verifies the **router** is allowlisted (`ctx.target`), the **input and output tokens** are allowlisted, the **recipient is the SMA itself** (`recipient == ctx.account`), the **amount** is `<= maxAmountPerTx`, and — if an oracle is configured — that `amountOutMinimum` is at least the oracle-derived minimum after slippage. Anything else returns `false` and the dispatch reverts with `PermissionDenied`. ## 4. Reconfigure or replace [Section titled “4. Reconfigure or replace”](#4-reconfigure-or-replace) * To change the bounds, the Permission Signer signs a fresh `Configure` (new nonce) — it clears the old config and applies the new one atomically. Submit via `factory.reconfigure(...)`. * To **tighten** safely without a front-run window, deploy/configure a new permission and `factory.replace(...)` (atomic kernel swap). See [the lifecycle](/protocol/permissions/lifecycle). Note The shipped templates are reference examples, not a fixed part of the protocol (see [Shared multi-tenant templates](/protocol/permissions/shared-templates)). To use this flow today, deploy the template yourself, or use one once it is published in the deployment manifests. You are responsible for the correctness of any permission you register — read and test a template’s `evaluate` logic before registering it in production. # Write your first permission We’ll build a minimal permission that authorizes ERC-20 `transfer` calls to a fixed recipient, and only to that recipient. It demonstrates every rule from the [implementation checklist](/protocol/permissions/ipermission#implementation-checklist): length-check, selector routing, target allowlist, value check, decode, fail-closed. ## The contract [Section titled “The contract”](#the-contract) ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {IPermission, Context} from "@sail/interfaces/IPermission.sol"; /// Authorizes ERC-20 transfer(to, amount) calls, but only to `allowedRecipient`, /// only on `allowedToken`, and only up to `maxAmount`. contract TransferToRecipientPermission is IPermission { bytes4 private constant TRANSFER_SELECTOR = 0xa9059cbb; // transfer(address,uint256) address public immutable allowedToken; address public immutable allowedRecipient; uint256 public immutable maxAmount; constructor(address token, address recipient, uint256 max) { allowedToken = token; allowedRecipient = recipient; maxAmount = max; } function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) { // 1. Token calls carry no ETH. if (ctx.value != 0) return false; // 2. Only the allowlisted token. if (ctx.target != allowedToken) return false; // 3. Only the transfer selector. if (ctx.selector != TRANSFER_SELECTOR) return false; // 4. Length check before decoding: 4 + 32 + 32 = 68 bytes. if (txData.length < 68) return false; // 5. Decode and check the arguments. (address to, uint256 amount) = abi.decode(txData[4:], (address, uint256)); if (to != allowedRecipient) return false; if (amount > maxAmount) return false; return true; } function discriminator() external pure returns (bytes32) { return keccak256("TransferToRecipientPermission"); } } ``` ## Why each line is there [Section titled “Why each line is there”](#why-each-line-is-there) * **`ctx.value != 0`** — an ERC-20 transfer should never carry ETH; a non-zero value is a red flag. * **`ctx.target`** — the call must go to the token you intend; otherwise a manager could call `transfer` on *any* contract. * **`ctx.selector`** — anything that isn’t `transfer` is denied (deny unknown selectors, don’t revert). * **length check before `abi.decode`** — decoding short calldata reverts; the kernel treats a revert as `false`, but an explicit check is clearer and cheaper. * **`to` / `amount` checks** — the actual bounds. Everything else was a guard so these checks are meaningful. * **`view` + no state writes** — required: the kernel calls this under `staticcall`. ## Gas [Section titled “Gas”](#gas) This permission does a handful of comparisons and one `abi.decode` — comfortably inside the 150,000-gas `PERMISSION_GAS_CAP`. If you add oracle reads or storage lookups, keep the total well under the cap (a cold `SLOAD` is 2,100 gas; an external oracle call can be 5–20k+). ## Test it off-chain first [Section titled “Test it off-chain first”](#test-it-off-chain-first) Before registering, you can confirm the permission would approve a given call without spending gas. With the Sailor harness this is `sailor mandate simulate`; at the protocol level you can `eth_call` the permission’s `evaluate` directly with a constructed `Context`. Only register a permission once you’ve verified it accepts the calls you intend and rejects the ones you don’t. ## Next [Section titled “Next”](#next) Now that you have a permission contract, you need an account to attach it to → [Deploy an SMA](/protocol/guides/deploy-an-sma). For the multi-account pattern (one deployment serving many accounts with per-account config), see [shared templates](/protocol/permissions/shared-templates). # Permission system A **permission** is a contract that answers one question: *is this call allowed?* The kernel asks it on every dispatch and respects the answer. Everything Sail can do — every venue, every bound, every strategy constraint — is expressed as a permission. This is where you build. This section covers: * [IPermission & Context](/protocol/permissions/ipermission) — the interface every permission implements, reproduced from source, field by field. * [Full expressiveness](/protocol/permissions/expressiveness) — why permissions are arbitrary Solidity rather than a constraint grammar, and what that buys. * [Shared multi-tenant templates](/protocol/permissions/shared-templates) — example implementations of the pattern: one deployment, per-account config, the opaque params blob, and the starter catalog. Reference examples, not drop-in production contracts. * [Permission lifecycle](/protocol/permissions/lifecycle) — registration, configuration, replacement, revocation, and manager rotation — each tied to the EIP-712 operation that authorizes it. * [Extension interfaces](/protocol/permissions/extensions) — optional introspection and agent-identity conventions for tooling. ## The mental model [Section titled “The mental model”](#the-mental-model) ```plaintext Permission Signer ── registers ──▶ permission address joins the account's mandate Manager ── dispatch names it ────▶ kernel staticcalls evaluate(txData, ctx) evaluate returns true ───────────▶ Safe executes · false/revert/OOG ─▶ dispatch reverts ``` The kernel never inspects *what* a permission checks. It guarantees the [four evaluation properties](/protocol/concepts/evaluation-guarantees) — static, gas-capped, selective, fail-closed — and leaves the meaning entirely to the permission author. Adding a new DeFi venue is therefore a **contract deployment, not a protocol upgrade**. # Full expressiveness Most access-control systems define a **constraint grammar** — a fixed vocabulary of rules the system knows how to evaluate (allowlists, amount caps, time windows). The grammar bounds what can be expressed, and extending it means changing the trusted system. Sail inverts the trade-off. The kernel knows **nothing** about DeFi venues. It calls `evaluate()` on a permission contract and respects the boolean. A permission is **arbitrary Solidity**, so it can express anything Solidity can compute within the gas budget: * decode a Uniswap `exactInputSingle` and check the router, both tokens, the amount cap, and an oracle-derived slippage bound; * read a lending protocol’s account data and reject a borrow that would exceed a target LTV; * require the swap recipient to be the SMA itself; * gate on `block.timestamp` for a vesting window; * require `ctx.manager` to equal a specific agent wallet. None of that vocabulary lives in the kernel. ## What this buys [Section titled “What this buys”](#what-this-buys) * **Permissionless venue support.** Adding a new DeFi integration is a **contract deployment, not a protocol upgrade**. No governance vote, no kernel change. * **Isolated audit surface.** The kernel is auditable on its own; each permission is auditable on its own. A bug in one permission is contained to the accounts that registered it. * **Future-proofing.** A venue or primitive that does not exist yet is supported the moment someone writes a permission for it. ## The boundary of responsibility [Section titled “The boundary of responsibility”](#the-boundary-of-responsibility) The kernel’s [four guarantees](/protocol/concepts/evaluation-guarantees) — `staticcall`, gas cap, selective authorization, fail-closed — protect the *kernel* from the permission. They do **not** make the permission correct. Everything inside that envelope is the author’s responsibility: > If a permission’s `evaluate` returns `true` for a call that drains the account, the kernel will execute it. The guarantees ensure the permission cannot harm the kernel or other accounts, and that a *failing* permission denies — but they cannot ensure a *permissive* permission is wise. This is the central trade-off to internalize before deploying: **register only permission contracts you have audited or trust.** The kernel binds authorization to a permission *address*; if that address is an upgradeable proxy, a change to its implementation needs no new signature (see [the lifecycle](/protocol/permissions/lifecycle) and [Security → limitations](/protocol/security/limitations)). Prefer non-upgradeable, audited permissions. # Extension interfaces Beyond `IPermission`, templates **may** implement optional interfaces that expose metadata to off-chain tooling. These are conventions for the tooling layer — **the kernel never reads or verifies any of them**, and they have no effect on whether a dispatch is permitted. ## IPermissionIntrospection [Section titled “IPermissionIntrospection”](#ipermissionintrospection) Stable identity and capability metadata for indexers and UIs: ```solidity interface IPermissionIntrospection { function permissionId() external view returns (bytes32); // template TYPE id, stable across deployments function permissionVersion() external view returns (bytes32); // bumps on breaking logic changes function metadataURI() external view returns (string memory); // ipfs:// , ar:// , or https URL ("" if none) function capabilityIds() external view returns (bytes32[] memory); // from SailCapabilities (non-empty) } ``` * `permissionId` identifies the template **type**, not the instance — two deployments of the same template return the same id. Convention: `keccak256("sail.permission..v1")`. Use the contract address to distinguish instances. * The kernel exposes `getPermissionsWithInfo(account)`, which reads `isBatchPermission()` and the introspection fields on each registered permission via `try/catch` (returning zero/false for non-implementers). It is an off-chain convenience view — never call it from dispatch. ## SailCapabilities [Section titled “SailCapabilities”](#sailcapabilities) Canonical capability identifiers, each `keccak256("sail.capability..v")`. Shipped templates declare one or more: | Constant | Declared by | | ----------------- | --------------------------------------------- | | `BOUNDED_SWAP` | `SharedBoundedSwapPermission` | | `BOUNDED_BORROW` | `SharedBoundedBorrowPermission` | | `TRANSFER_TARGET` | `SharedTransferTargetPermission` | | `DEFI_BUNDLE` | `SharedDeFiBundlePermission` | | `PENDLE_YIELD` | `SharedPendlePermission` | | `AMM_LIQUIDITY` | `SharedAMMLiquidityPermission` | | `BATCH_DISPATCH` | `SharedApproveAndCallBatchPermission` | | `AGENT_IDENTITY` | any template exposing agent-identity metadata | Third parties may define their own ids following the same convention; the hash space makes collisions impractical. ## Agent identity [Section titled “Agent identity”](#agent-identity) Two optional resolvers expose an `AgentIdentityRef` (namespace, chainId, identity registry, agentId, agent wallet) for off-chain discovery: * `IAgentIdentityResolver.agentIdentity()` — one fixed identity for a single-account/fixed-agent template. * `IAccountAgentIdentityResolver.agentIdentityFor(account)` — per-account identity for shared templates (returns a zero struct when unset; must not revert). `BaseSharedPermission` implements this and lets the Permission Signer set it via `setAgentIdentity` / `setAgentIdentityDirect`. Note **Identity is metadata, not authorization.** The kernel never reads agent identity. A template that wants to *enforce* an identity constraint (e.g. require `ctx.manager == agentWallet`) must do so inside its own `evaluate()`. Reputation, validation, and curation-registry integration are explicitly out of scope; live ERC-8004 registry resolution is a possible future extension. ## IBatchPermission [Section titled “IBatchPermission”](#ibatchpermission) The one extension the kernel *does* call — for batch dispatch. A batch-aware template implements `evaluateBatch(Call[], BatchContext) → bool` and `isBatchPermission() → true` (the kernel staticcalls the latter to detect batch support). See [single & batch dispatch](/protocol/architecture/dispatch). # IPermission & Context Every Sail permission implements a single interface. It is small on purpose. ```solidity interface IPermission { /// Decide whether a manager-submitted transaction is permitted. function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool); /// Optional stable identifier for off-chain indexing/deduplication. function discriminator() external view returns (bytes32); } ``` * `evaluate` is `view` and is called by the kernel via `staticcall` under `PERMISSION_GAS_CAP` (150,000 gas). A revert or gas exhaustion is treated as `false`. It may read arbitrary on-chain state within the gas budget but can never mutate state. * `discriminator` returns a stable identifier — by convention `keccak256("")` for fixed-shape permissions, or `bytes32(0)` for generic/multi-purpose ones. It is metadata for off-chain tooling; the kernel does not use it for authorization. ## The Context struct [Section titled “The Context struct”](#the-context-struct) The kernel passes a read-only snapshot of the dispatch environment, captured at the moment of evaluation. Reproduced verbatim from `contracts/interfaces/IPermission.sol`: ```solidity struct Context { address account; // the Safe account whose assets are being moved address manager; // the delegated signer who submitted the dispatch address submitter; // msg.sender of the dispatch; may differ from manager (relayer) address target; // the call target address bytes4 selector; // leading 4 bytes of calldata; bytes4(0) if calldata < 4 bytes uint256 value; // native ETH forwarded with the call (wei) uint256 blockTimestamp; // block.timestamp at dispatch — for time-based gates uint256 blockNumber; // block.number at dispatch } ``` Field-by-field: | Field | Use it to… | | -------------------------------- | -------------------------------------------------------------------------------------------- | | `account` | scope per-account config (`mapping(account => …)`); confirm a swap’s `recipient == account`. | | `manager` | enforce identity-based rules (e.g. require a specific agent wallet). | | `submitter` | gate on the relayer/submitter if your threat model needs it. | | `target` | allowlist the contract being called. | | `selector` | route by function; reject unknown selectors. | | `value` | reject unexpected ETH on ERC-20 calls. | | `blockTimestamp` / `blockNumber` | time- or block-based windows and rate limits. | The raw calldata of the call being dispatched arrives as `txData` (so `txData[:4] == ctx.selector` when length ≥ 4). ## Implementation checklist [Section titled “Implementation checklist”](#implementation-checklist) From the integration guidance and the shipped templates: 1. **Check calldata length before decoding.** `abi.decode` on short calldata reverts (treated as `false`); guard with explicit length checks first. 2. **Return `false` for unknown selectors** — don’t revert; just deny. 3. **Validate the target** against an allowlist unless the permission is intentionally selector-only. 4. **Check `ctx.value`** — token calls should carry no ETH; a non-zero `value` is suspicious. 5. **Return `false` for malformed or out-of-bounds input** — never assume well-formed calldata. 6. **Stay well under 150k gas** — leave margin for decoding, memory, and any on-chain reads. As a rough guide: staticcall overhead \~3k, a 2-arg decode \~1–2k, a cold `SLOAD` 2,100, an oracle call 5–20k+. 7. **Implement `discriminator()`** — `keccak256("YourPermissionName")`, or `bytes32(0)` if generic. A minimal permission: ```solidity contract AllowTargetPermission is IPermission { address public immutable allowedTarget; constructor(address t) { allowedTarget = t; } function evaluate(bytes calldata, Context calldata ctx) external view returns (bool) { return ctx.target == allowedTarget; } function discriminator() external pure returns (bytes32) { return keccak256("AllowTargetPermission"); } } ``` Walk through a real one in [Write your first permission](/protocol/guides/write-a-permission). # Permission lifecycle Every change to an account’s mandate is authorized by the **Permission Signer** through an EIP-712 signature, and (for shared templates) configuration is authorized the same way. Each operation consumes the account’s `signerNonces` (configuration consumes the template’s own `configNonces`), and each carries a `deadline`. See the [EIP-712 reference](/protocol/reference/eip712) for exact type strings. ## Registration [Section titled “Registration”](#registration) The Permission Signer signs a `RegisterPermission(account, permission, nonce, deadline)` message. The kernel checks the permission is a deployed contract, isn’t already registered, and that the account is below `maxPermissionsPerAccount`; it then appends the permission and charges the flat registration fee (excess ETH refunded). Use `registerPermissions(...)` to add several atomically under one nonce. ```solidity kernel.registerPermission{value: fee}(account, permission, deadline, signerSig); ``` ## Configuration & reconfiguration [Section titled “Configuration & reconfiguration”](#configuration--reconfiguration) For a shared template, the Permission Signer signs a `Configure(account, keccak256(params), nonce, deadline)` message; anyone may submit it (the `MandateFactory` is the canonical submitter, but holds no privilege). A subsequent `configure` with a fresh nonce **clears previous state and applies the new params atomically**. Registration and configuration are independent — registration adds the address to the mandate; configuration sets that address’s per-account rules. Note **Sequencing.** Dispatch authorizes by permission *address*, not by configuration *state*. To **tighten** a permission’s rules safely, prefer `replacePermission` (atomic swap to a freshly-configured permission) over reconfiguring in place — an in-place `configure` has a front-run window while the transaction is pending. ## Atomic replacement [Section titled “Atomic replacement”](#atomic-replacement) `replacePermission(account, old, new, deadline, sig)` swaps one permission for another in a single signed operation (fee charged for the new one). `replacePermissions(...)` does N→N atomically, eliminating the front-running overlap window that separate revoke + register calls would create. Both bump the manager/batch nonce epochs. ## Revocation — two levels [Section titled “Revocation — two levels”](#revocation--two-levels) * **Single permission:** `revokePermission` / `revokePermissions` removes a permission, narrowing the manager’s authority. * **Whole session:** `revokeSession` sets `sessionActive = false`, blocking **all** dispatch for the account until `activateSession` (which needs a fresh signature, proving the key is still controlled). Both take effect in a single block and **invalidate every outstanding manager-signed dispatch** that predates them (via the nonce-epoch bump). Revocation is intentionally available **even while the protocol is paused**, so owners can always reduce exposure. ## Manager rotation [Section titled “Manager rotation”](#manager-rotation) The Safe itself (`msg.sender == account`) calls `setManager(newManager)`. Rotation **clears the entire permission set atomically** — no mandate silently carries over to a new manager — and bumps the manager/batch nonce epochs to invalidate anything the old manager pre-signed. The owner then re-registers each permission for the new manager. Rotation is exempt from the pause (key loss is exactly when recovery must remain possible) and moves no funds. ## Fee policy changes [Section titled “Fee policy changes”](#fee-policy-changes) `setFeePolicy(account, newFeePolicy, feeAsset, deadline, sig)` updates the account’s fee policy and canonical fee asset. The new policy must be on governance’s `trustedFeePolicy` allowlist. Setting `newFeePolicy = address(0)` clears it (allowed even while paused, so a compromised policy can be disarmed) and blocks fee collection. ## A note on upgradeable permissions [Section titled “A note on upgradeable permissions”](#a-note-on-upgradeable-permissions) The kernel binds authorization to a permission **address**. If a registered permission is an upgradeable proxy, changing its implementation requires **no new kernel signature**. Register only non-upgradeable or audited permission contracts. (See whitepaper §8.2 and [Security → limitations](/protocol/security/limitations).) # Shared multi-tenant templates The naive pattern — one permission contract per account, parameters in the constructor — gives a clean per-instance audit surface but is expensive in gas and bytecode. The **shared multi-tenant template** pattern shown here serves every account that registers it from one contract deployed once per chain, with per-account configuration stored in mappings keyed by `account`. Note **These templates are examples, not the protocol.** The `Shared*` templates and `BaseSharedPermission` are reference implementations that demonstrate how to implement the [`IPermission`](/protocol/permissions/ipermission) pattern across common DeFi primitives. They are illustrative, not a fixed part of the trusted core — anyone can deploy their own permission contracts, and the kernel registers and dispatches any contract implementing `IPermission`. You are responsible for the correctness of any permission you register (see [permission correctness is the author’s responsibility](/protocol/security/limitations)). Treat these templates as starting points to read, learn from, and adapt — not audited, drop-in production contracts. The seven shared templates are deployed against the current kernel (see [addresses](/protocol/reference/addresses)). Six were included in the [Octane security review](/protocol/security/audits); the seventh, `WithdrawPermission`, was rewritten afterward (v2) and is not covered by it. All remain reference implementations; verify and test before any production use. ## BaseSharedPermission [Section titled “BaseSharedPermission”](#basesharedpermission) Shared templates inherit `BaseSharedPermission`, which provides the per-account configuration machinery so subclasses only implement their decode/check logic. ```solidity abstract contract BaseSharedPermission is IConfigurablePermission, IAccountAgentIdentityResolver, EIP712, ReentrancyGuard { bytes32 public constant CONFIGURE_TYPEHASH = keccak256("Configure(address account,bytes32 paramsHash,uint256 nonce,uint256 deadline)"); mapping(address => uint256) public configNonces; mapping(address => bool) public isConfigured; function _applyConfig(address account, bytes calldata params) internal virtual; // subclass hook } ``` It exposes two ways to configure an account, both reading the authoritative `permissionSigner` from the kernel: * `configure(account, params, deadline, sig)` — anyone may submit; authorization is the Permission Signer’s EIP-712 signature over `Configure(account, keccak256(params), nonce, deadline)`. The per-account `configNonces` is incremented only **after** the signature verifies. * `configureDirect(account, params)` — `msg.sender` must equal the account’s `permissionSigner` (useful when the signer is an EOA submitting the tx itself). A fresh `configure` clears the account’s previous config and applies the new one atomically. ## The opaque params blob [Section titled “The opaque params blob”](#the-opaque-params-blob) `params` is an ABI-encoded blob whose structure each template defines. The kernel and the base contract treat it as opaque bytes; the subclass decodes it in `_applyConfig`. For `SharedBoundedSwapPermission`, for example: ```solidity abi.encode( address[] routers, address[] tokensIn, address[] tokensOut, uint256 maxAmountPerTx, uint256 maxSlippageBps, address priceOracle, uint256 maxPriceAgeSec ) ``` The template decodes this, validates it (e.g. an oracle requires a non-zero freshness bound, else `MissingPriceAge`), rebuilds its O(1) allowlist mappings for that account, and stores the slot. ## Starter catalog [Section titled “Starter catalog”](#starter-catalog) A set of shared templates ships with the protocol as demonstrations of the pattern — **unaudited references**, not part of the trusted core. Each declares a capability id from `SailCapabilities`: | Template | Gates | Capability | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `SharedBoundedSwapPermission` | AMM swaps: router + token allowlists, per-tx amount cap, optional oracle slippage check (Uniswap V3 `exactInputSingle` V1/V2, V2 `swapExactTokensForTokens`). | `bounded-swap.v1` | | `SharedBoundedBorrowPermission` | Borrows on Aave V3 / Morpho / Compound with LTV enforcement. | `bounded-borrow.v1` | | `SharedTransferTargetPermission` | ERC-20 `transfer`/`transferFrom` to allowlisted recipients. | `transfer-target.v1` | | `SharedDeFiBundlePermission` | Composite swap + borrow + transfer, selector-routed. | `defi-bundle.v1` | | `SharedPendlePermission` | Pendle V2 Router V4: liquidity, PT/YT swaps, mint/redeem, claim. | `pendle-yield.v1` | | `SharedAMMLiquidityPermission` | Concentrated-liquidity ops on Uniswap V3 / Aerodrome. | `amm-liquidity.v1` | | `SharedApproveAndCallBatchPermission` | Batch-only: the `approve → call → reset` pattern. | `batch-dispatch.v1` | Anyone may deploy additional templates for any venue — the catalog above is a starting set, not a closed list. ## Standalone (clone) templates [Section titled “Standalone (clone) templates”](#standalone-clone-templates) Some templates are single-account and use `initialize(...)` instead of `configure(...)`. The `MandateFactory.deployAndAttach` flow clones such a logic contract (EIP-1167), initializes it, and registers the clone — one transaction, deterministic address. See [MandateFactory](/protocol/architecture/mandate-factory). Configure one end to end in [Use a shared template](/protocol/guides/use-a-template). # Overview ![](/brand/banner-protocol.jpg) Sail Protocol is a minimal account-abstraction primitive for **onchain separately managed accounts (SMAs)**, implemented for the EVM. It wraps a [Safe](https://safe.global) smart account with a permission-gating layer so that a designated **manager** — typically an autonomous agent — can execute transactions on the account’s behalf, but only within bounds the account owner has authorized on-chain. ## The problem [Section titled “The problem”](#the-problem) A separately managed account is the structure professional asset management is built on: capital is titled to the owner, a manager executes within a mandate, and the mandate can be narrowed or revoked at any time. DeFi never reproduced it. Three gaps: * **Pooled vaults give up custody and attribution.** Depositors mint shares of a single strategy, surrender account-level custody, and inherit the strategy’s full risk profile shared across every other depositor. * **Smart wallets have no mandate.** Self-custodial wallets preserve custody but have no native concept of a manager bounded by enforceable rules. * **Agents need signing authority *and* enforceable bounds.** An autonomous agent managing capital must be able to transact continuously, yet must be constrained by something stronger than a prompt. Sail fills the gap between them: a self-custodial account whose manager has bounded, code-enforced, revocable authority. ## How it works [Section titled “How it works”](#how-it-works) ![](/assets/how-it-works.png) * Capital is held in the owner’s **Safe**. The kernel never holds assets. * The **mandate** is not a document — it is the set of permission contracts registered for the account. * On each dispatch the manager’s signature **names one** registered permission. The kernel evaluates only that permission, via `staticcall` under a fixed gas cap, and forwards the call to the Safe only if it returns `true`. * Because permissions are arbitrary Solidity, **any DeFi primitive can be expressed as a permission**. Adding a new venue is a contract deployment, not a protocol upgrade. ## What’s in the trusted core [Section titled “What’s in the trusted core”](#whats-in-the-trusted-core) | Component | Role | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SailKernel` | Account registration, permission registry, EIP-712 signature verification, manager dispatch (single & batch), fee accounting, principal tracking. The only trusted execution surface. | | `SailGovernance` | Parameter store behind a 48-hour timelock: constitutional caps, the protocol cut, the registration fee, and the trusted Safe/fee-policy allowlists. Emergency pause with auto-expiry. | | `TimelockController` | Standalone OpenZeppelin timelock (48h), deployed separately and injected into `SailGovernance`, which validates it at construction. | | `MandateFactory` | UX orchestrator bundling configure → register into one transaction. Holds no privilege; every inner call is independently signature-authenticated. | | `SafeModuleEnabler` | Stateless one-shot helper that enables the kernel as a Safe module during account creation. | | `StandardFeePolicy` | Reference fee policy: management fee on AUM plus a performance fee above a per-account high-water mark. | Permission templates and fee policies live **outside** the trusted core. A bug in a template affects only the accounts that registered it. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * New to the model? Start with [Concepts](/protocol/concepts). * Want the component-level picture? See [Architecture](/protocol/architecture). * Building a permission? Jump to the [Permission system](/protocol/permissions) and [Guides](/protocol/guides). * Need addresses or signatures? See [Reference](/protocol/reference). Caution The trusted core and shared templates are deployed on 12 chains (10 mainnets + 2 testnets). The trusted core and the templates as they stood at the review were reviewed by [Octane](https://www.octane.security), an AI source-code security scanner, across three analyses; the later `WithdrawPermission` v2 rewrite is not covered (see [Security](/protocol/security)). A security review is not a proof of correctness — do not use with funds you are not prepared to lose. # Reference Precise, source-verified reference material: * [Deployment addresses](/protocol/reference/addresses) — the trusted core, identical on every supported chain, plus the supported-chain list and the external dependencies. * [Deterministic addresses](/protocol/reference/deterministic-addresses) — the CREATE2 salts, the `boundSalt` formula, and how to predict an SMA address off-chain. * [EIP-712 typed data](/protocol/reference/eip712) — every type string and struct hash the kernel verifies. * [Contract reference](/protocol/reference/contracts) — the public surface of the trusted core, function by function. The canonical source of truth for addresses and signatures is the [Protocol repository](https://github.com/sail-money/Protocol) at the deployed commit; this reference mirrors it. # Deployment addresses Both the trusted core and the seven shared permission templates deploy via **deterministic CREATE2 with a global (chain-independent) salt per contract**, through the standard CREATE2 factory `0x4e59b44847b379578588920cA78FbF26c0B4956C`. The practical consequence: **every core contract and every template has the same address on every chain**, and so does the resulting SMA. The [Protocol repository](https://github.com/sail-money/Protocol/blob/main/deployments/addresses.md) is the canonical source of truth. ## Core addresses (identical on every chain) [Section titled “Core addresses (identical on every chain)”](#core-addresses-identical-on-every-chain) | Contract | Address | | -------------------- | -------------------------------------------- | | `SailKernel` | `0x38b508756c976e876EFF05a29E731A4d348BA6ED` | | `SailGovernance` | `0x4315B37cA4A315A7042af1Fcb37F8436f4D24356` | | `TimelockController` | `0xC1E5F9A581D4100Aa949f80204540a33aD97A7b6` | | `MandateFactory` | `0x6d2C802ffa0d9A8Ed69A5Bf22c1b63ccB566B8Fc` | | `StandardFeePolicy` | `0x1087312447C8a2BfA15EB9cE23590E3502DBA04b` | | `SafeModuleEnabler` | `0x7897Cb53a4be4a2eaAf46D60573C4Fd83b33fE1F` | ## Shared permission templates (identical on every chain) [Section titled “Shared permission templates (identical on every chain)”](#shared-permission-templates-identical-on-every-chain) Multi-tenant templates — one deployment per chain serves every account, bound to the canonical core kernel above (constructor `(kernel, author)`, `author` = the deployer EOA below). | Template | Address | | ------------------------------- | -------------------------------------------- | | `SwapPermission` | `0x35cEEa0db96997Cc3CF3beB42FFa36A499342F7C` | | `SwapPermissionNoOracle` | `0x34Ba96CbEd1f46c88A5265E645DC5fe41662b519` | | `BorrowPermission` | `0x3e2666051599223cEAb10De55C89A0842857d8AF` | | `DepositPermission` | `0xBfB5e13a97b12Ee89d2F2b9B65eCf7e0E371911f` | | `WithdrawPermission` | `0xB8A6CC40466c0C33a230f87a1EBC368568B96269` | | `TransferPermission` | `0xda909a1CC584fb7559Ce4A828b008B473Da095e1` | | `ApproveAndCallBatchPermission` | `0x0535A4D51333484ef583103DAB1a9449756ab732` | ### Superseded templates (still live on-chain) [Section titled “Superseded templates (still live on-chain)”](#superseded-templates-still-live-on-chain) | Template | Address | Notes | | ------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `WithdrawPermission` (v1) | `0xF5eF5dda450a130e3020d54f565E830e4a7531f8` | The original ERC-20-transfer withdraw gate (`transfer` / `transferFrom` to one allowed recipient). Replaced in place by the vault-exit `WithdrawPermission` (v2) above, deployed under a rotated salt (`sail.template.withdraw.v2`) so the two do not collide. | The v1 contract is **not** disabled or revoked — templates are immutable and the protocol has no kill switch — but Sailor no longer references it, and no account ever registered it. New registrations use the v2 address. The two are **not** config-compatible (the blob changed from `(address[] tokens, address allowedRecipient, uint256 maxAmountPerTx)` to `(address[] targets, address[] tokens, uint256 maxAmountPerTx)`), and the introspection identity was bumped to `sail.permission.WithdrawPermission.v2` so consumers can tell them apart. Counting the superseded v1, eight withdraw-family contracts exist on-chain — but the shared-template set is still **seven**. Note The shared templates are **reference implementations** (see [Security](/protocol/security)), not audited drop-in production contracts. You are responsible for the correctness of any permission you register — read, verify, and test before production use. See [permission correctness is the author’s responsibility](/protocol/security/limitations). ## Governance & config (identical on every chain) [Section titled “Governance & config (identical on every chain)”](#governance--config-identical-on-every-chain) | Role | Address | Notes | | -------------------- | -------------------------------------------- | -------------------------------------------- | | Admin Safe (3/5) | `0x152a32c851d317Cd54F1E6423377d7D58Dd3DE8C` | parameter governance behind the 48h timelock | | Treasury Safe (3/5) | `0x7b37F85575F1568a37dBA342BC5FE6d393F0872f` | protocol fee recipient | | Emergency Safe (2/3) | `0xFf02DE6630F192Bc6d14608f5C52a9f1ae478961` | emergency pause (auto-expiry + cooldown) | | Deployer EOA | `0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6` | deployment only; holds no protocol authority | ## Supported chains [Section titled “Supported chains”](#supported-chains) All chains run the selective-dispatch kernel via CREATE2 and have had their onboarding allowlists bootstrapped (`allowlistBootstrapped() == true`). | Chain | Chain ID | Native | Type | | ------------ | -------- | ------ | ----------- | | Ethereum | 1 | ETH | mainnet | | Base | 8453 | ETH | mainnet | | Arbitrum | 42161 | ETH | mainnet | | Optimism | 10 | ETH | mainnet | | Unichain | 130 | ETH | mainnet | | World | 480 | ETH | mainnet | | MegaETH | 4326 | ETH | mainnet | | Robinhood | 4663 | ETH | mainnet | | BSC | 56 | BNB | mainnet | | HyperEVM | 999 | HYPE | mainnet | | Base Sepolia | 84532 | ETH | **testnet** | | Eth Sepolia | 11155111 | ETH | **testnet** | That’s **12 chains — 10 mainnets and 2 testnets.** The CREATE2 factory (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) and the Safe v1.4.1 proxy factory (`0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67`) are present at their canonical addresses on all twelve, so the same-address property holds on each. Robinhood (4663) was deployed by replaying the exact CREATE2 salt + init code recovered from the original Base transactions, so its core and template addresses are byte-for-byte identical to every other chain. ## Fees (live) [Section titled “Fees (live)”](#fees-live) | Fee | Value | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Permission registration fee (live) | `0.00015 ETH` on the 10 ETH-native chains (Robinhood included); `0.00045 BNB` on BSC; `0.005 HYPE` on HyperEVM | | Registration-fee cap (immutable) | `MAX_PERMISSION_FEE_WEI` = `0.01` native-unit ceiling per chain | | Protocol cut on manager fees | `0` at launch (immutable cap `MAX_PROTOCOL_CUT_BPS` = 2500 bps = 25%) | | Management / performance / distributor fees | `0` at launch | The registration fee was deployed at `0.00015` native on **every** chain (CREATE2 requires byte-identical constructor arguments, so the address is only reproducible with the same fee). Governance later raised the **live** rate on BSC and HyperEVM via the 48h timelock — which does not change the already-locked contract address. See [Fees](/protocol/fees-and-governance/fees). ## External dependencies (canonical, present on all chains) [Section titled “External dependencies (canonical, present on all chains)”](#external-dependencies-canonical-present-on-all-chains) | Contract | Address | | ------------------------ | -------------------------------------------- | | CREATE2 factory | `0x4e59b44847b379578588920cA78FbF26c0B4956C` | | Safe v1.4.1 ProxyFactory | `0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67` | | Safe v1.4.1 singleton | `0x41675C099F32341bf84BFc5382aF534df5C7461a` | | SafeL2 v1.4.1 singleton | `0x29fcB43b46531BcA003ddC8FCB67FFE91900C762` | These are the values seeded into the kernel’s trusted allowlists (`trustedSafeFactory`, `trustedSafeSingleton`, `trustedModuleSetup` → the `SafeModuleEnabler` above, `trustedFeePolicy` → the `StandardFeePolicy` above, plus the Safe-proxy runtime codehash). For this deploy, governance is the admin Safe, so allowlists were seeded post-deploy via `SailGovernance.bootstrapAllowlists()` (a one-shot, non-timelocked latch) rather than at genesis. Note **Security review.** The trusted core and the shared templates **as they stood at the review** were reviewed by [Octane](https://www.octane.security), an AI source-code security scanner, across three analyses; the final analysis found no critical- or high-severity findings. The `WithdrawPermission` v2 rewrite postdates that review and is **not** covered by it (internal test coverage only). A review is not a proof of correctness — do not use with funds you are not prepared to lose. See [Security](/protocol/security). # Contract reference The public surface of the trusted core, function by function. Signatures are mirrored from the deployed source; see the [Protocol repository](https://github.com/sail-money/Protocol) for full NatSpec. ## SailKernel [Section titled “SailKernel”](#sailkernel) `EIP712("SailKernel","1")`, `ReentrancyGuard`. GPL-2.0-or-later. ### Account instantiation [Section titled “Account instantiation”](#account-instantiation) | Function | Notes | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `createAccount(address safeFactory, address safeSingleton, bytes safeInitializer, uint256 saltNonce, address permissionSigner, address manager, address feePolicy, address feeAsset) → address` | Deploy (or adopt) a Safe via a trusted factory and register it. | | `registerAccount(address permissionSigner, address manager, address feePolicy, address feeAsset)` | Register an existing Safe. `msg.sender` must be the Safe (codehash + module checks). | | `setManager(address newManager)` | Rotate the manager. `msg.sender` must be the Safe. Clears the mandate; bumps nonce epochs. | | `getManager(address account) → address` | Current manager. | ### Permission registry (permission-signer EIP-712 + deadline) [Section titled “Permission registry (permission-signer EIP-712 + deadline)”](#permission-registry-permission-signer-eip-712--deadline) | Function | Notes | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | `registerPermission(address account, address permission, uint256 deadline, bytes sig) payable` | Add one; charges the fee. | | `registerPermissions(address account, address[] permissions, uint256 deadline, bytes sig) payable` | Add many under one nonce. | | `revokePermission(address account, address permission, uint256 deadline, bytes sig)` | Remove one. Allowed while paused. | | `revokePermissions(address account, address[] permissions, uint256 deadline, bytes sig)` | Remove many. | | `replacePermission(address account, address oldPermission, address newPermission, uint256 deadline, bytes sig) payable` | Atomic swap. | | `replacePermissions(address account, address[] oldPermissions, address[] newPermissions, uint256 deadline, bytes sig) payable` | Atomic N→N swap. | | `revokeSession(address account, uint256 deadline, bytes sig)` | Suspend dispatch. | | `activateSession(address account, uint256 deadline, bytes sig)` | Resume dispatch. | | `setFeePolicy(address account, address newFeePolicy, address feeAsset, uint256 deadline, bytes sig)` | Change/clear the policy. | | `getPermissions(address account) → address[]` · `isPermissionRegistered(address,address) → bool` · `getPermissionsWithInfo(address) → PermissionInfo[]` | Views. | ### Dispatch & fees [Section titled “Dispatch & fees”](#dispatch--fees) | Function | Notes | | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | | `dispatch(address account, address permission, address target, uint256 value, bytes data, bytes managerSig, uint256 deadline)` | Single call, selective auth. | | `dispatchBatch(address account, address permission, Call[] calls, bytes managerSig, uint256 deadline)` | Atomic batch via a batch-aware permission. | | `previewBatch(address account, address permission, Call[] calls) → (bool, string)` | Off-chain pre-flight (no sig/session/deadline check). | | `collectFees(address account, uint256 grossFee, uint256 currentNav, address feeToken)` | Manager / Safe / permissionSigner may call; enforces the policy ceiling + protocol split. | | `recordDeposit(address account, uint256 amount)` · `recordWithdrawal(address account, uint256 amount)` | Informational; permissionSigner only. | | `setTreasury(address newTreasury)` | `onlyTimelock`. | | `hashTypedDataV4(bytes32 structHash) → bytes32` | EIP-712 digest helper. | Key public constants: `PERMISSION_GAS_CAP = 150_000`, `BATCH_EVAL_GAS_CAP = 1_000_000`, `MAX_BATCH_LENGTH = 16`, plus the `*_TYPEHASH` constants ([EIP-712 reference](/protocol/reference/eip712)). Public mappings: `configs`, `registered`, `managerNonces`, `batchNonces`, `signerNonces`, `cumulativeDeposits`, `cumulativeWithdrawals`, `treasury`, `governance`. ## SailGovernance [Section titled “SailGovernance”](#sailgovernance) GPL-2.0-or-later. Immutable caps: `MAX_PROTOCOL_CUT_BPS = 2500`, `MAX_PERMISSIONS_CAP = 100`, `MAX_PERMISSION_FEE_WEI` (≤ 0.01 native), `REQUIRED_TIMELOCK_DELAY = 48 hours`, `PAUSE_COOLDOWN = 72 hours`. | Function | Notes | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | `setProtocolCutBps(uint256)` · `setPermissionRegistrationFee(uint256)` · `setMaxPermissionsPerAccount(uint256)` | `onlyTimelock`, bounded by caps. | | `setTrustedSafeFactory / setTrustedSafeSingleton / setTrustedFeePolicy / setTrustedModuleSetup(address,bool)` · `setTrustedSafeProxyCodehash(bytes32,bool)` | `onlyTimelock`. | | `bootstrapAllowlists(address[],address[],address[],address[],bytes32[])` | `onlyGovernance`, once, pre-timelock; latches `allowlistBootstrapped`. | | `proposeGovernance(address)` · `acceptGovernance()` · `rotateTimelockRoles(address oldGov, address newGov)` | Two-step transfer + role rotation. | | `pause()` · `unpause()` (emergency admin) · `rotateEmergencyAdmin(address)` (timelock) · `isPaused() → bool` | 72h auto-expiry pause. | Public state: `currentProtocolCutBps`, `permissionRegistrationFee`, `maxPermissionsPerAccount`, `governance`, `pendingGovernance`, `emergencyAdmin`, `timelock`, `pauseExpiry`, the `trusted*` mappings, `allowlistBootstrapped`. ## StandardFeePolicy [Section titled “StandardFeePolicy”](#standardfeepolicy) `IFeePolicy`. GPL-2.0-or-later. Caps: management ≤ 1000 bps, performance ≤ 5000 bps, distributor ≤ 10000 bps; `MIN_COLLECTION_INTERVAL = 1 days`. | Function | Notes | | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | `feeRecipient() → address` | Returns the `feeManager`. | | `computeFee(address account, uint256 currentNav) → (uint256 grossFee, address distributor, uint256 distributorBps)` | Management + performance over HWM. | | `recordCollection(address account, uint256 grossFee, uint256 currentNav)` | `onlyKernel`; updates HWM/timestamp; requires HWM seeded. | | `seedHighWaterMark(address account, uint256 initialNav)` | `onlyFeeManager`; must precede first collection. | | `setManagementFeeBps / setPerformanceFeeBps / setDistributor / setDistributorBps` | `onlyFeeManager`, bounded. | | `proposeFeeManager(address)` · `acceptFeeManager()` | Two-step transfer. | ## MandateFactory [Section titled “MandateFactory”](#mandatefactory) `ReentrancyGuard`. GPL-2.0-or-later. No privilege; every inner call is signature-authenticated. `attach`, `attachBatch`, `reconfigure`, `replace`, `deployAndAttach` (+ `predictCloneAddress(address impl, bytes32 salt) → address`), `detach`, `detachBatch`. `receive()` accepts ETH only from the kernel. ## SafeModuleEnabler [Section titled “SafeModuleEnabler”](#safemoduleenabler) MIT. Stateless. `enable(address module)` — must be invoked via `Safe.setup`’s delegatecall; resolves `address(this)` to the Safe and calls `enableModule(module)`. ## Interfaces [Section titled “Interfaces”](#interfaces) `IPermission`, `IBatchPermission`, `IConfigurablePermission`, `IFeePolicy`, `IOracle`, `IPermissionIntrospection`, `IAgentIdentityResolver` / `IAccountAgentIdentityResolver`, and the `SailCapabilities` library. See [IPermission & Context](/protocol/permissions/ipermission) and [extension interfaces](/protocol/permissions/extensions). # Deterministic addresses How Sail achieves identical core addresses across chains, and identical SMA addresses across chains — with the exact formulas from `SailKernel.createAccount`. ## Core salts [Section titled “Core salts”](#core-salts) Each core contract is deployed through the standard CREATE2 factory (`0x4e59b44847b379578588920cA78FbF26c0B4956C`) with a chain-independent salt and identical constructor arguments: | Contract | Salt | | ------------------ | ------------------------------------- | | TimelockController | `keccak256("sail.timelock.v1")` | | SailGovernance | `keccak256("sail.governance.v1")` | | SailKernel | `keccak256("sail.kernel.v1")` | | MandateFactory | `keccak256("sail.mandatefactory.v1")` | | StandardFeePolicy | `keccak256("sail.feepolicy.v1")` | | SafeModuleEnabler | `keccak256("sail.modulenabler.v1")` | A CREATE2 address is `keccak256(0xff ++ factory ++ salt ++ keccak256(initCode))[12:]`. Since the factory and salt are identical everywhere, the address is identical **iff the init code (constructor args included) is identical**. Injecting the timelock into `SailGovernance` (rather than constructing it inline) is what makes every constructor argument chain-independent. ## SMA address binding [Section titled “SMA address binding”](#sma-address-binding) When the kernel creates an account it derives the Safe’s CREATE2 salt by binding the caller’s nonce to the principals: ```solidity boundSalt = uint256(keccak256(abi.encode( saltNonce, msg.sender, permissionSigner, manager, feePolicy ))); ``` It then predicts the Safe proxy address with the exact formula the Safe v1.4.1 `SafeProxyFactory` uses, so an already-deployed proxy at that address is adopted rather than re-deployed: ```solidity // create2Salt = keccak256(keccak256(initializer), boundSalt) bytes32 create2Salt = keccak256(abi.encodePacked(keccak256(safeInitializer), boundSalt)); // initCodeHash = keccak256(proxyCreationCode ++ uint256(uint160(singleton))) bytes32 initCodeHash = keccak256(abi.encodePacked( ISafeFactory(safeFactory).proxyCreationCode(), uint256(uint160(safeSingleton)) )); address predicted = address(uint160(uint256(keccak256(abi.encodePacked( bytes1(0xff), safeFactory, create2Salt, initCodeHash ))))); ``` Because the `safeInitializer` references only chain-identical contracts (the kernel and the `SafeModuleEnabler`), the same `(owner, permissionSigner, manager, feePolicy, saltNonce)` yields the **same SMA address on every supported chain**. ## Front-run resistance [Section titled “Front-run resistance”](#front-run-resistance) Binding the principals into `boundSalt` means a deployment supplying a different `permissionSigner` or `manager` lands at a **different** address. An attacker therefore cannot register *your* counterfactual address with *their* principals — the address itself is a commitment to the principals (Octane #4 / #16). ## Predicting off-chain [Section titled “Predicting off-chain”](#predicting-off-chain) To compute an SMA address before deployment, reproduce the two-step derivation above with your chosen `saltNonce` and principals and the canonical Safe v1.4.1 factory/singleton from [addresses](/protocol/reference/addresses). The Sailor SDK exposes this as `account predict`; at the protocol level, mirror the Solidity above. You can fund the predicted address before the Safe exists on a given chain. # EIP-712 typed data Every signed operation in Sail is EIP-712 typed data. This page lists the exact type strings the kernel verifies (from `SailKernel`), plus the template `Configure` type from `BaseSharedPermission`. All type hashes are `keccak256` of the type string shown. Caution These are the **current, selective-model** type strings. They differ from older revisions: `Dispatch` now carries a `permission` field, and every registry operation carries a `deadline`. Always read the kernel’s public `*_TYPEHASH` constants (or `DISPATCH_TYPEHASH()`) on-chain rather than hardcoding — the Sailor SDK detects them for you. ## Domain [Section titled “Domain”](#domain) ```plaintext EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) name = "SailKernel" version = "1" chainId = verifyingContract = ``` `kernel.hashTypedDataV4(structHash)` applies this domain on-chain; equivalently `keccak256("\x19\x01" ++ domainSeparator ++ structHash)`. ## Manager operations (signed by `manager`) [Section titled “Manager operations (signed by manager)”](#manager-operations-signed-by-manager) ```plaintext Dispatch(address account,address permission,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline) DispatchBatch(address account,address permission,bytes32 callsHash,uint256 nonce,uint256 deadline) ``` * `dataHash = keccak256(data)`; `nonce = managerNonces[account]`. * `callsHash = keccak256(abi.encode(calls))`; `nonce = batchNonces[account]`. ## Permission-signer operations (signed by `permissionSigner`) [Section titled “Permission-signer operations (signed by permissionSigner)”](#permission-signer-operations-signed-by-permissionsigner) ```plaintext RegisterPermission(address account,address permission,uint256 nonce,uint256 deadline) RevokePermission(address account,address permission,uint256 nonce,uint256 deadline) ReplacePermission(address account,address oldPermission,address newPermission,uint256 nonce,uint256 deadline) RegisterPermissions(address account,address[] permissions,uint256 nonce,uint256 deadline) RevokePermissions(address account,address[] permissions,uint256 nonce,uint256 deadline) ReplacePermissions(address account,address[] oldPermissions,address[] newPermissions,uint256 nonce,uint256 deadline) RevokeSession(address account,uint256 nonce,uint256 deadline) ActivateSession(address account,uint256 nonce,uint256 deadline) SetFeePolicy(address account,address newFeePolicy,address feeAsset,uint256 nonce,uint256 deadline) ``` All use `nonce = signerNonces[account]`. ## Template configuration (signed by `permissionSigner`, verified by the template) [Section titled “Template configuration (signed by permissionSigner, verified by the template)”](#template-configuration-signed-by-permissionsigner-verified-by-the-template) ```plaintext Configure(address account,bytes32 paramsHash,uint256 nonce,uint256 deadline) SetAgentIdentity(address account,bytes32 identityHash,uint256 nonce,uint256 deadline) ``` * `paramsHash = keccak256(params)`; `nonce = template.configNonces(account)`. * Verified against the EIP-712 domain of the **template** (each shared template is its own `EIP712` domain, e.g. `name = "SharedBoundedSwapPermission"`, `version = "1"`), not the kernel. ## Encoding `address[]` [Section titled “Encoding address\[\]”](#encoding-address) For `RegisterPermissions` / `RevokePermissions` / `ReplacePermissions`, an `address[]` field is encoded per EIP-712 §4 as the keccak256 of the ABI-encoded, zero-padded addresses: ```solidity bytes32 arrHash = keccak256(abi.encodePacked(/* each address as bytes32 */)); ``` (The kernel’s `_hashAddressArray` builds a `bytes32[]` of zero-padded addresses and hashes the packed result.) ## Building a digest (example: Dispatch) [Section titled “Building a digest (example: Dispatch)”](#building-a-digest-example-dispatch) ```solidity bytes32 structHash = keccak256(abi.encode( kernel.DISPATCH_TYPEHASH(), account, permission, target, value, keccak256(data), kernel.managerNonces(account), deadline )); bytes32 digest = kernel.hashTypedDataV4(structHash); // sign `digest` with the manager key (ECDSA), or have an ERC-1271 signer attest it ``` Signatures are verified with `_recoverOrERC1271`: ECDSA first, then ERC-1271 fallback for contract signers — so managers and permission signers may be EOAs, multisigs, or smart accounts. # Security Sail’s security model is “a small trusted core that contains an open periphery.” Read it in three parts: * [Guarantees](/protocol/security/guarantees) — the six properties the deployed bytecode provides. * [Limitations](/protocol/security/limitations) — what the protocol does **not** protect against, stated plainly. * [Octane security review](/protocol/security/audits) — the three AI source-code security analyses, and how to report a vulnerability. To report a vulnerability: ****. Caution The trusted core and shared templates are deployed on 12 chains (10 mainnets + 2 testnets). The trusted core and the templates **as they stood at the review** were reviewed by [Octane](https://www.octane.security), an AI source-code security scanner, across three analyses; the final analysis found no critical- or high-severity findings. The later `WithdrawPermission` v2 rewrite is **not** covered (see [Octane security review](/protocol/security/audits)). A security review is not a proof of correctness — do not use with funds you are not prepared to lose. # Octane security review The trusted core and the shared permission templates **as they stood at the review** were reviewed by **[Octane](https://www.octane.security)**, an AI source-code security scanner, across **three successive analyses** (2026-06-24, 2026-06-26, 2026-06-29). The **third and final analysis (2026-06-29) identified no critical- or high-severity findings**; all reported vulnerabilities were resolved or acknowledged, and the remaining lower-severity warnings are documented, accepted by design, or out of scope. ## What was reviewed [Section titled “What was reviewed”](#what-was-reviewed) Both the trusted core — `SailKernel`, `SailGovernance`, its `TimelockController`, and the core interfaces — **and** the shared permission templates as they stood at the review: `SwapPermission`, `SwapPermissionNoOracle`, `BorrowPermission`, `DepositPermission`, the then-current `WithdrawPermission`, `TransferPermission`, and `ApproveAndCallBatchPermission`. **Review boundary.** The analyses cover `main` through PR #79 (commit `8d1e122`). Scope was defined by files and contracts; the reviewer did not state a line-count figure. Caution **`WithdrawPermission` v2 is not covered by this review.** After the review, `WithdrawPermission` was rewritten in place — from an ERC-20-transfer gate into a bounded vault / lending-pool exit permission (v2). That rewrite **postdates the Octane analyses and was not part of them**; its on-chain NatSpec states verbatim that “it has not been externally reviewed.” It carries internal coverage only — **80 tests across three suites** (44 unit, 18 adversarial stress, 18 independent red-team), all passing. The other six shared templates are unchanged from what the review covered. See [WithdrawPermission](/sailor/guides/configure-a-template#withdrawpermission-v2-bounded-position-exits). ## Reports [Section titled “Reports”](#reports) The reports are Octane’s signed deliverables, published as PDFs in the repository. The most recent (2026-06-29) reflects the current state of the reviewed code; earlier reports are retained for transparency. | Analysis | Date | Pages | Report | | ------------------- | ---------- | :---: | --------------------------------------------------------------------------------------------------------------- | | Third (most recent) | 2026-06-29 | 153 | [PDF](https://github.com/sail-money/Protocol/raw/main/docs/security/octane-security-analysis-03-2026-06-29.pdf) | | Second | 2026-06-26 | 175 | [PDF](https://github.com/sail-money/Protocol/raw/main/docs/security/octane-security-analysis-02-2026-06-26.pdf) | | First | 2026-06-24 | 108 | [PDF](https://github.com/sail-money/Protocol/raw/main/docs/security/octane-security-analysis-01-2026-06-24.pdf) | Several findings map to mechanisms in the deployed bytecode and are referenced in [Guarantees](/protocol/security/guarantees) — for example **#1** (trusted module-setup allowlist, closing the arbitrary setup-delegatecall surface during `createAccount`), **#4 / #4a / #4b** (principal-bound CREATE2 salt and proxy-codehash / module-enabled checks), **#7** (nonce-epoch invalidation of pre-signed dispatches), and **#16** (counterfactual addresses cannot be squatted with different principals). ## Not a guarantee [Section titled “Not a guarantee”](#not-a-guarantee) A security review is **not** a proof of correctness. It reduces risk in the reviewed code; it does not eliminate it, and it does not extend to code deployed by users. In particular, the correctness of any user-deployed permission or fee policy remains the author’s responsibility — see [Limitations](/protocol/security/limitations). ## Scope boundary [Section titled “Scope boundary”](#scope-boundary) The trusted core is the primary review surface. The shared permission templates and the reference fee policy were included in the review, but they remain **reference implementations** outside the trusted core: their blast radius is bounded to the accounts that opt into them. Register them only if you choose to trust them. ## Reporting a vulnerability [Section titled “Reporting a vulnerability”](#reporting-a-vulnerability) Email ****. Please include enough detail to reproduce, and allow time for a fix before public disclosure. The contracts carry a `@custom:security-contact security@sail.money` tag in their source, and the canonical [Security Policy](https://github.com/sail-money/Protocol/blob/main/SECURITY.md) lives at the repository root. # Guarantees The protocol provides six guarantees as properties of the **deployed bytecode** — they hold regardless of what any permission or fee policy does. ### 1. Custody isolation [Section titled “1. Custody isolation”](#1-custody-isolation) The kernel cannot transfer Safe assets except through a manager dispatch that satisfies the named permission’s evaluation. The kernel has no direct write access to the Safe outside the module dispatch path, and a dispatch whose `target` is the Safe itself is rejected (`AccountSelfTarget`) — blocking module-triggered self-reconfiguration. ### 2. Selective authorization [Section titled “2. Selective authorization”](#2-selective-authorization) A dispatch succeeds only if the permission named in the manager’s signature is registered for the account **and** returns true on evaluation. An unregistered permission reverts before evaluation (`PermissionNotRegistered`); there is no implicit allow-all state (deny-by-default). ### 3. Reentrancy safety [Section titled “3. Reentrancy safety”](#3-reentrancy-safety) Permission evaluation occurs via `staticcall`, which prohibits state mutation — no re-entry path exists through the permission surface. The kernel’s state-changing entry points are additionally `nonReentrant`. ### 4. Gas isolation [Section titled “4. Gas isolation”](#4-gas-isolation) Each permission is called under a fixed gas cap (`PERMISSION_GAS_CAP = 150_000`; `BATCH_EVAL_GAS_CAP = 1_000_000` for batches). Exceeding it is treated as returning false. A malicious permission cannot deny service to the kernel or drain the manager’s gas budget beyond the cap. ### 5. Constitutional fee caps [Section titled “5. Constitutional fee caps”](#5-constitutional-fee-caps) The protocol cut (`MAX_PROTOCOL_CUT_BPS = 2500`) and registration fee (`MAX_PERMISSION_FEE_WEI ≤ 0.01` native) cannot be exceeded under any governance procedure — they are immutable. The protocol cut is zero at launch; the registration fee is live at a small non-zero rate (see [Fees](/protocol/fees-and-governance/fees)). ### 6. Signer separation [Section titled “6. Signer separation”](#6-signer-separation) The Permission Signer cannot move Safe assets (it never signs a dispatch). The Manager cannot register or revoke permissions (it never signs a registry op). The Safe owner can always revoke the manager (`setManager`) and can remove the kernel module entirely. ## Supporting mechanisms [Section titled “Supporting mechanisms”](#supporting-mechanisms) These reinforce the six guarantees: * **Fail-closed evaluation** — any false/revert/OOG/malformed return reverts the whole dispatch. * **Nonce-epoch invalidation** — any restrictive signer op or manager rotation bumps the manager/batch nonce epoch (`1 << 128`), invalidating all of the manager’s outstanding pre-signed dispatches. * **Front-run-resistant account creation** — the CREATE2 salt binds the principals, so a counterfactual SMA address cannot be claimed with different principals. * **Trusted-component allowlists** — only allowlisted Safe factories, singletons, module-setup helpers, proxy codehashes, and fee policies can be used, blocking attacker-supplied setup delegatecalls (Octane #1) and metamorphic fee policies. * **Three nonce namespaces** — dispatch, batch, and signer operations cannot replay across each other. * **Emergency pause** that still permits de-risking operations (revoke, rotate) while blocking value movement. Several of these correspond to findings raised in the Octane security analyses (e.g. Octane #1, #4/#4a/#4b, #7, #16) and are addressed in the deployed bytecode. See [Octane security review](/protocol/security/audits). # Limitations Honest statements of what Sail does **not** protect against. None of these are bugs — they are deliberate boundaries of the trusted core. Understanding them is a prerequisite to using the protocol safely. ## 1. Permission correctness is the author’s responsibility [Section titled “1. Permission correctness is the author’s responsibility”](#1-permission-correctness-is-the-authors-responsibility) The kernel guarantees a permission cannot harm the kernel or other accounts, and that a *failing* permission denies. It does **not** guarantee a permission is *correct*. If a registered permission’s `evaluate` returns `true` for a call that drains the account, the kernel will execute that call. Consequences: * **Register only audited or trusted permission contracts.** A permissive bug in a permission is a loss for the accounts that registered it. * **Upgradeable permissions are a trust escalation.** The kernel binds authorization to a permission *address*. If that address is an upgradeable proxy, changing its implementation requires **no new kernel signature** — so a registered upgradeable permission can later behave differently than when it was approved. Prefer non-upgradeable permissions. (Whitepaper §8.2.) * The seven shared templates are deployed against the current kernel (see [addresses](/protocol/reference/addresses)). Six of them were included in the [Octane security review](/protocol/security/audits); the seventh, `WithdrawPermission`, was rewritten afterward (v2 — bounded vault/pool exits) and is **not** covered by that review (internal test coverage only). All remain **reference implementations** — treat them as starting points to read, verify, and test, not as audited drop-in production contracts. ## 2. Manager-attested NAV (the self-managed-SMA framing) [Section titled “2. Manager-attested NAV (the self-managed-SMA framing)”](#2-manager-attested-nav-the-self-managed-sma-framing) `collectFees` takes a `currentNav` supplied by the **manager**; the kernel does not verify it. The kernel enforces only that the requested `grossFee` does not exceed the fee policy’s computed maximum — but that maximum is itself a function of the manager-supplied NAV. A dishonest manager could inflate `currentNav` to unlock a larger fee ceiling. * `StandardFeePolicy` trusts the NAV by design. This is appropriate for a **self-managed SMA**, where the owner *is* (or fully trusts) the manager. * For a manager the owner does **not** fully trust, use a fee policy that validates NAV through an oracle inside `computeFee`, rather than the reference policy. * Denomination must be consistent: `grossFee`, `currentNav`, and the `feeAsset` must use the same units, or the fee ceiling is meaningless. ## 3. Off-chain venue components are out of the kernel’s view [Section titled “3. Off-chain venue components are out of the kernel’s view”](#3-off-chain-venue-components-are-out-of-the-kernels-view) A permission can only check what is visible on-chain at dispatch time. It cannot reason about off-chain components of a venue — sequencer behavior, off-chain order books, oracle update cadence beyond the freshness bound it checks, or MEV in the surrounding transaction. Oracle-gated templates check `updatedAt` freshness and (should) gate on L2 sequencer uptime, but the quality of those protections is only as good as the oracle and the bounds the permission author configures. ## Also worth knowing [Section titled “Also worth knowing”](#also-worth-knowing) * **No layered “AND” composition in the kernel.** Selective authorization evaluates exactly one named permission per dispatch. Compound rules must be expressed inside a single permission (or a batch permission), not by requiring several permissions to jointly approve. * **Force-sent ETH is unrecoverable** in `MandateFactory` (no sweep function); an accepted residual since `receive()` already blocks normal deposits. * **Governance is trusted within its caps.** Governance cannot exceed the constitutional caps or move user funds, but it does control tunable parameters and the trusted allowlists, behind the 48-hour timelock. If you find something here that looks like more than a documented limitation, report it: ****. # CLI reference `sailor` is the operator command surface. Install it with the [package](/sailor/packages) and run via `npx sailor `, or globally: ```bash npm install -g @sail.money/sailor sailor --help sailor --version ``` Every command supports `--help`; most support `--json` for machine-readable output. The commands below are grouped by workflow and verified against the Sailor source. ## Conventions [Section titled “Conventions”](#conventions) * **`--json`** — almost every command accepts `--json` for machine-readable output. Use it in scripts, CI, and when [operating via a coding agent](/sailor/getting-started/coding-agent). * **Read-only vs. gas** — `capabilities`, `chains`, `doctor`, `status`, `scan`, and `mandate simulate` spend no gas. Commands that change on-chain state route owner signatures through the browser [signing server](/sailor/concepts/keys-and-custody). * **`SAIL_PASSPHRASE`** — unlocks the encrypted manager key non-interactively (CI/headless). Read from `.sail/.env.local` or the environment; never commit it. * **RPC resolution** — `.sail/.env.local` chain-specific var → generic `RPC_URL` → shell env. See [Multi-chain operation](/sailor/guides/multi-chain). ## Project setup [Section titled “Project setup”](#project-setup) | Command | What it does | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `sailor init [dir]` | Scaffold a new agent project (`--template `, `--chain `, `--rpc-url `, `--force` to re-init). | | `sailor update` | Re-sync agent tooling files (skills, `AGENTS.md`, `Dockerfile`) from the latest template — your code and state are untouched. | ## Keys and owner [Section titled “Keys and owner”](#keys-and-owner) | Command | What it does | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `sailor keys generate` | Generate + encrypt a key (`--type agent-wallet` or `mandate-signer`; `--passphrase`, else `SAIL_PASSPHRASE`, else prompt; `--force`). | | `sailor keys show` | Addresses of stored keys. | | `sailor keys export-ci` | Export key material for CI use. | | `sailor owner connect` | Open the signing server, wait for your wallet, save it as owner (`--timeout `). | | `sailor owner show` | Show the saved project owner. | ## SMA lifecycle [Section titled “SMA lifecycle”](#sma-lifecycle) | Command | What it does | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sailor account predict` | Deterministic SMA address before deploying (`--owner`, `--salt`, `--chain`). | | `sailor onboard` | Set up an SMA end to end (`--new-sma` to create, `--sma
` to reuse, `--template ` to register a permission, `--skip-mandate`, `--salt `). | | `sailor account deploy-chain` | Deploy the same SMA address on an additional chain (same owner/manager/salt). | | `sailor account rotate-signer` | Rotate the delegated agent wallet and re-approve mandates (`--to`, `--generate`, `--skip-reattach`, `--reattach-only`, `--list`). | | `sailor scan` | Discover the owner’s SMAs, permissions, and local keys (`--owner
`). | | `sailor status` | Current account, permission, and session status. | ## Mandate lifecycle [Section titled “Mandate lifecycle”](#mandate-lifecycle) | Command | What it does | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sailor mandate templates` | How to author your own permission + any community-deployed addresses. | | `sailor mandate deploy` | Deploy a Foundry-compiled permission via the signing UI (`--contract ` or `--artifact `, `--args`/`--args-file`, `--build`, `--attach --sma
`). | | `sailor mandate register` | Register already-deployed permission(s) on an SMA — a comma-separated list is one signature (`--label`). (`sailor mandate attach` is a deprecated alias.) | | `sailor mandate configure` | Configure a shared template’s per-account bounds (`--template --args-file ` or `--params `; `--simulate-only` for a gas-free preflight; `--force`). | | `sailor mandate simulate` | Probe a permission’s `evaluate()` off-chain — no gas, no signing (`--target/--calldata/--value/--expect/--label`, or `--calls ` for a batch). | | `sailor mandate sign` | Review and confirm the permissions authorized for your SMA (`--yes` for CI). | | `sailor mandate prepare` | Prepare a mandate draft for review/signing in the UI. | | `sailor mandate revoke` | Revoke permission(s) — owner-authorized (`--address ` or `--all`). | | `sailor mandate list` | Permissions deployed from this project. | | `sailor mandate update` | Update tracked-permission metadata (`--name`, `--source-path`, `--artifact-path`). | | `sailor mandate deploy-clone` | Deploy + register a standalone clone permission — currently unavailable (no clone templates deployed); use `mandate deploy`. | ## Signing server [Section titled “Signing server”](#signing-server) | Command | What it does | | ------------------------------- | ----------------------------------------------------------------------------- | | `sailor signer start` | Start the persistent browser-signing daemon (blocks — run in the background). | | `sailor signer status` / `stop` | Inspect / stop it. (`sailor station …` is a deprecated alias.) | ## Run and automate [Section titled “Run and automate”](#run-and-automate) | Command | What it does | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sailor run` | The agent execution loop (`--once` for a single tick, `--chain `). | | `sailor service install` | Install the agent as an OS service that restarts on crash — launchd / systemd / Task Scheduler (`--interval `, `--project `, `--chain `). | | `sailor service status` / `stop` / `uninstall` / `logs` | Manage the installed service. | | `sailor trigger github` | Fire the scaffold’s GitHub Actions agent workflow on demand (`--workflow`, `--ref`, `--reason`, `--repo`). | | `sailor session pause` / `resume` | Instantly revoke / restore the agent’s dispatch rights — Safe custody untouched. | ## Dashboard [Section titled “Dashboard”](#dashboard) | Command | What it does | | --------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `sailor ui start` | Local dashboard (per-project port in 3333–3999; `--expose tailscale` serves it HTTPS on your tailnet — never public). | | `sailor ui stop` / `status` | Stop / inspect it. See [Dashboard](/sailor/dashboard). | ## Shipyard (simulation sandbox) [Section titled “Shipyard (simulation sandbox)”](#shipyard-simulation-sandbox) Local forks of the real chains, with fake money, on a second dashboard of their own. Requires Foundry (`anvil`). Full guide: [Shipyard](/sailor/shipyard). | Command | What it does | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `sailor sandbox start` | Start the Shipyard dashboard on its own port, rooted at `.shipyard/sandbox/` (bare `sailor sandbox` does the same). | | `sailor sandbox stop` | Stop the dashboard and its forks, saving chain state so the next start resumes the same world (`--keep-forks` stops only the dashboard). | | `sailor sandbox status` | Show whether the Shipyard dashboard is running. | `sailor shipyard …` is an alias of `sandbox`, so either spelling works. ## Diagnostics [Section titled “Diagnostics”](#diagnostics) | Command | What it does | | --------------------- | -------------------------------------------------------------------------------------- | | `sailor doctor` | Kernel health, dispatch model, RPC reachability, gas balances (`--account
`). | | `sailor capabilities` | What you can build on this chain — read-only, no gas. | | `sailor chains` | Supported chains + kernel addresses (`--verify` checks each via `eth_getCode`). | # Concepts The mental model for operating with Sailor. These pages assume you’ve met the Protocol’s [core concepts](/protocol/concepts) — they map Sailor onto that model rather than redefining it. * [Sailor & the Protocol](/sailor/concepts/protocol-mapping) — the same vocabulary (SMA, mandate, permission, manager, dispatch), and which side of the line Sailor sits on. * [On-chain vs off-chain](/sailor/concepts/on-chain-off-chain) — financial bounds are enforced on-chain; cadence and orchestration are the operator’s concern. Where the line sits, plainly. * [Keys & custody](/sailor/concepts/keys-and-custody) — owner, permission signer, and manager keys, and how Sailor handles each. # Keys & custody Sailor operates the Protocol’s [three roles](/protocol/concepts/roles). Each maps to a key Sailor handles differently — and the differences are the heart of the safety model. | Role | Key | Where it lives | Who signs with it | | --------------------- | ----------------------------------- | ------------------------------------------------------- | ----------------------------------- | | **Owner** | Your wallet (MetaMask, Rabby, …) | Your browser wallet — **never read by Sailor** | You, in the browser signing server | | **Permission Signer** | Same as owner, or a separate signer | Your wallet / signer | You, authorizing mandate operations | | **Manager** (agent) | The agent wallet | `.sail/keys/manager.json`, encrypted (geth keystore v3) | Sailor, automatically, per dispatch | ## The owner key is never in the terminal [Section titled “The owner key is never in the terminal”](#the-owner-key-is-never-in-the-terminal) Owner and permission-signer signatures (creating the SMA, registering or revoking permissions) are deliberate, custody-affecting actions. Sailor routes them through a local **signing server** — an HTTP + WebSocket daemon that serves a browser UI: ```bash sailor signer start & # the signing daemon sailor owner connect # connect your wallet in the browser, persist it as owner ``` The agent (and any coding assistant operating Sailor) **never holds the owner key**. It pushes signing requests to the signing server; you approve them in the browser; then the agent submits the transactions it’s allowed to. This is why a coding agent can drive setup without ever being trusted with custody. ## The manager key is encrypted and scoped [Section titled “The manager key is encrypted and scoped”](#the-manager-key-is-encrypted-and-scoped) The manager (agent) key is generated with `sailor keys generate` and stored encrypted at `.sail/keys/manager.json` (scrypt + aes-128-ctr). It signs dispatches — and **only** dispatches. By the [signer-separation guarantee](/protocol/security/guarantees), the manager cannot register or revoke permissions and cannot exceed any registered permission’s bounds. Unlock it non-interactively (CI, headless runs) by setting `SAIL_PASSPHRASE`; never commit it. Addresses passed to the CLI are normalized to EIP-55 checksum before any on-chain call or state write. ## Custody never leaves the Safe [Section titled “Custody never leaves the Safe”](#custody-never-leaves-the-safe) Capital is held in the owner’s Safe. Sailor cannot move it except through a manager dispatch that satisfies a registered permission — and even then the funds move via the Safe’s own module path, not through Sailor. You can sever the agent instantly without touching custody: ```bash sailor session pause # revoke dispatch rights; sailor session resume to restore ``` Pausing the session blocks every dispatch while leaving the Safe, its balances, and the mandate intact. Rotating the agent key (`sailor account rotate-signer`) clears the mandate and re-approves it for the new key — see [CLI → SMA lifecycle](/sailor/cli#sma-lifecycle). Note Custody is protected by construction, but **a mandate is only as correct as its permission contracts**. The key model prevents the agent from exceeding the mandate; it does not make a permissive mandate safe. See [Build & register a mandate](/sailor/guides/build-a-mandate) and the Protocol’s [security limitations](/protocol/security/limitations). # On-chain vs off-chain The single most important line to internalize when operating with Sailor: > **All financial bounds are enforced on-chain. Cadence and orchestration are off-chain, operator-side concerns.** ## What the chain enforces [Section titled “What the chain enforces”](#what-the-chain-enforces) The kernel evaluates the named [permission](/protocol/permissions) on **every** dispatch. Anything a permission checks — allowed venues, token allowlists, per-transaction amount caps, slippage bounds, recipients, LTV — is enforced on-chain, fail-closed, before any state change. No off-chain bug in Sailor can cause a dispatch that the registered permissions don’t allow. If your agent tries to do something outside the mandate, the kernel reverts it. This is why the mandate is the security boundary: once it’s signed, the agent can run autonomously, because the *worst* it can do is bounded by contracts you approved. ## What the operator controls off-chain [Section titled “What the operator controls off-chain”](#what-the-operator-controls-off-chain) Sailor (and your agent code) controls everything the chain does **not** decide: * **Cadence** — how often the agent ticks (`sailor run` schedule, the GitHub Actions cron). The chain doesn’t know or care how frequently you dispatch. * **Which allowed action to take, and when** — your `tick()` decides *whether* to swap today; the permission decides whether a swap is *allowed at all*. * **Inputs and data** — price feeds, signals, and any data your strategy reads. Sailor bakes in no third-party data; you supply your own (see [the Agent interface](/sailor/sdk/agent)). * **Submission** — gas, nonces, relaying. Authority comes from the manager’s signature, not the submitter, so a relayer can submit. ## A concrete example [Section titled “A concrete example”](#a-concrete-example) Suppose you want an agent that dollar-cost-averages USDC into WETH, at most 100 USDC per trade, once a day, only on Uniswap V3. * **On-chain (the permission):** router = Uniswap V3, `tokenIn = USDC`, `tokenOut = WETH`, `maxAmountPerTx = 100e6`, recipient = the SMA. The kernel enforces these on every dispatch. * **Off-chain (your agent):** “once a day,” “buy when my signal says so,” “skip if balance is low.” None of this is the kernel’s concern — it’s your `tick()` logic and your `sailor run` schedule. If a bug makes your agent try to buy 1,000 USDC or swap on the wrong router, the permission returns false and the dispatch reverts. The cap is real because it’s on-chain; the schedule is best-effort because it’s off-chain. Keep this split in mind when designing: **put every rule that protects capital into a permission**, and leave only timing and selection to the agent. ## Two modes, one codebase [Section titled “Two modes, one codebase”](#two-modes-one-codebase) The dashboard runs in one of two modes: live, or **Shipyard**, the simulation sandbox. They are not a flag on a shared server. Each is a separate process running the same server code, pointed at a different state root (`.sail/` versus `.shipyard/sandbox/`) on a different port, and the fork-lifecycle routes exist only on the Shipyard instance. Nothing in the live process can read or write the Shipyard root, and nothing in Shipyard can reach the live one. That separation is what makes the boundary above testable: in Shipyard the kernel, the permission contracts, and the Safe are the real deployed contracts, carried in by a local fork of the chain, so a dispatch is evaluated by the same code that would evaluate it on mainnet. Only the money is fake. See [Shipyard](/sailor/shipyard). # Sailor & the Protocol Sailor builds on the Protocol’s model — it isn’t a different one. Most terms mean exactly what the [Protocol glossary](/protocol/concepts/glossary) says. Two of them — **template** and **mandate** — are the operational terms that emerge once you actually *build and deploy* permissions with the SDK and CLI; this page pins down precisely what they map to on-chain. | Term | Meaning | In Sailor | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [SMA](/protocol/concepts/smas) | A Safe registered with `SailKernel`; holds the capital self-custodially. | `.sail/account.json`; created via `sailor onboard --new-sma`; SDK `client.account`. | | [Permission](/protocol/permissions) | A single bounded rule — one class of action the manager is whitelisted to take (e.g. *“swap USDC→WETH up to 10k per call on this router”*), with its limits. A permission is a *rule*, not a deployed thing on its own; it is carried by a template and enforced by a mandate. | Authored/configured in the Foundry workspace. | | [Template](/protocol/permissions/shared-templates) | A set of permissions that has **not** been deployed — a reusable blueprint you build real mandates from. Think *class*: a reference, not a live object. | `sailor mandate templates`; the `template` field on a mandate; SDK `PermissionTemplate`. Treated as [examples](/sailor/guides/configure-a-template). | | [Mandate](/protocol/concepts/mandate) | A template (or a set of permissions built from scratch) **deployed into a smart contract** and attached to the SMA via the kernel’s registry. Think *object*: the live, on-chain instance of a template. It is an `IPermission` implementation the kernel holds an address to and consults on every dispatch. | `sailor mandate *`; SDK `client.mandate`; the signed `.sail/mandate.json` the runner executes against. Each has an on-chain `address` (see `mandates.json`). | | [Manager](/protocol/concepts/roles) | The key that signs dispatches — here, your agent. | The encrypted agent wallet at `.sail/keys/manager.json`. | | [Dispatch](/protocol/architecture/dispatch) | A manager-signed call the kernel evaluates against one named mandate and executes if it’s within bounds. | `sailor run`; SDK `client.dispatch.single` / `.batch`. | ## Template → mandate: class vs object [Section titled “Template → mandate: class vs object”](#template--mandate-class-vs-object) The distinction is the whole point of Sailor’s build step: * A **template** is a *set of permissions that has not been deployed*. It’s source you read, adapt, and parameterize — a blueprint. On its own it controls nothing. * A **mandate** is what you get when that template (or a from-scratch set of permissions) is **deployed into a smart contract** — an `IPermission` instance — and **attached to your SMA** through the kernel’s permission registry. It’s the live object: it has an on-chain address, and the kernel consults it to decide whether a manager’s call is allowed. `sailor mandate deploy/attach` is exactly this build step: it turns a template into a deployed, registered mandate. Sailor’s `mandates.json` reflects it directly — each mandate record carries both its on-chain `address` and the `template` it was built from. So the layering is: ```plaintext permission (a rule) → template (a set of rules, undeployed) → mandate (that set, deployed & attached to the SMA) ``` An SMA’s full set of attached mandates is the complete description of what its manager may do. The manager never has open-ended control: it can only do what some attached mandate already whitelists, and only the kernel — never Sailor — decides whether a given call qualifies. ## Which side of the line is Sailor on? [Section titled “Which side of the line is Sailor on?”](#which-side-of-the-line-is-sailor-on) ```plaintext on-chain (trusted core) off-chain (Sailor) ┌─────────────────────────────────────────┐ ┌──────────────────────────────────┐ │ SailKernel · SailGovernance · Safe SMA │ ◀ │ CLI · SDK · agent runner · UI │ │ evaluates mandates, moves assets │ │ builds & signs, schedules, reads │ └─────────────────────────────────────────┘ └──────────────────────────────────┘ ``` Sailor builds transactions, signs EIP-712 messages, and submits them. Every authority decision — *is this call allowed?* — is made by the kernel on-chain against the deployed mandate. Sailor cannot widen what a mandate permits; it can only help you express, deploy, and operate it. ## Sailor helps you express bounds — it never closes the menu [Section titled “Sailor helps you express bounds — it never closes the menu”](#sailor-helps-you-express-bounds--it-never-closes-the-menu) The Protocol’s [full-expressiveness](/protocol/permissions/expressiveness) property carries over: a mandate is arbitrary Solidity (an `IPermission` implementation), so any rule you can express in code can be a permission — Sailor never forces a specific template or limits what you can express. The shipped templates are starting points; you can author any permission set, deploy it as a mandate, and Sailor will simulate, attach, and dispatch against it. Sailor’s job is to make expressing and operating fully-bounded mandates fast — not to constrain the set of expressible bounds. Continue to [on-chain vs off-chain](/sailor/concepts/on-chain-off-chain). # Dashboard (local UI) `sailor ui` runs a **local web dashboard** for your project — onboarding, wallet gas balances, mandate health, activity, and owner signing. It reads from the project’s `.sail/` directory and talks to your RPC; there is **no hosted backend** and nothing is sent anywhere. ```bash sailor ui start # start the dashboard (prints its URL) sailor ui status # is it running, and where sailor ui stop # stop it ``` ## What it shows [Section titled “What it shows”](#what-it-shows) * **Onboarding** — a guided path for SMA deployment and first mandate, mirroring the CLI/skills flow. * **Gas balances** — the native-token (gas) balance and top-up status of the **owner wallet** and the **agent wallet**, so you can keep them funded to sign and submit. The dashboard does **not** display the SMA’s token holdings; a **View portfolio** link opens your SMA on [DeBank](https://debank.com) to review what it holds, and a **Manage SMA** link opens it in the Safe app. * **Mandate health** — the permissions registered on the SMA and their configured bounds. * **Activity** — the append-only `.sail/activity.jsonl` feed (dispatches, denials, collections). * **Owner signing** — the browser **signing server**, where owner/permission-signer EIP-712 signatures happen in your wallet. Sailor never reads the owner key. ## Ports & exposure [Section titled “Ports & exposure”](#ports--exposure) Each project gets a **deterministic port in 3333–3999** derived from its path — read the URL `sailor ui start` prints, or `.sail/runtime/ui.json`; don’t assume 3333. The signing server runs alongside it. By default the dashboard binds to localhost only. To reach it from another device on your own network, `--expose tailscale` serves it over HTTPS on your tailnet — it is **never** exposed publicly: ```bash sailor ui start --expose tailscale ``` Note **Under Docker** the UI binds to a fixed **3334** inside the container (signing server **3141**); with `docker run -P` these map to random host ports — resolve them with `docker port agent 3334`. See [Docker → Dashboard access](/sailor/docker#dashboard-access). # Docker Run Sailor from the pre-built [`sailmoney/sailor`](https://hub.docker.com/r/sailmoney/sailor) image — **no Node.js on the host**, an isolated environment, the same CLI. The image is built from the repo’s `Dockerfile` (linux/amd64 + linux/arm64, published with SBOM and provenance attestations) and contains exactly the files npm publishes. Tags: `latest` and per-release pinned versions. ## Start a project [Section titled “Start a project”](#start-a-project) ```bash mkdir my-agent && cd my-agent docker run -d --name agent -P -v "${PWD}:/workspace" sailmoney/sailor docker exec agent sailor init ``` * The container idles (`sleep infinity`) and exists to serve `docker exec agent sailor `. * `-v "${PWD}:/workspace"` mounts your project **on the host** — read and edit files with your normal editor or coding agent; only `sailor` commands need the `docker exec` prefix. `sailor init` detects the container (`SAILOR_INSTALL_MODE=docker`). * Naming the container something else? Match it: `docker run --name myproject -e SAILOR_CONTAINER_NAME=myproject …`. ## Key handling [Section titled “Key handling”](#key-handling) Keys are generated **inside the container** but written to `.sail/keys/` **on your host** via the mount — encrypted at rest (geth keystore v3), never baked into the image or its layers: ```bash docker exec -it agent sailor keys generate --type agent-wallet ``` Use `-it` for interactive passphrase entry, or pass `-e SAIL_PASSPHRASE=…` at `docker run` time for headless use (env vars are visible to `docker inspect` — prefer interactive entry on shared hosts). The container runs as the unprivileged `node` user. ## Dashboard access [Section titled “Dashboard access”](#dashboard-access) Inside the container the UI binds to **3334** (and the signing server to **3141**). With `-P`, Docker maps them to random host ports — resolve before opening the browser: ```bash docker exec agent sailor ui start docker port agent 3334 # → 0.0.0.0:49201 → open http://localhost:49201 ``` Prefer fixed ports? Publish them explicitly instead of `-P`: ```bash docker run -d --name agent -p 3334:3334 -p 3141:3141 -v "${PWD}:/workspace" sailmoney/sailor ``` ## Headless / unattended operation [Section titled “Headless / unattended operation”](#headless--unattended-operation) The agent loop is just another exec — schedule it with whatever supervises your containers: ```bash docker exec agent sailor run --once # one tick (cron-friendly) docker exec -d agent sailor run # detached loop inside the container ``` `sailor service install` targets host OS service managers and is **not** applicable inside the container — use your orchestrator’s restart policy (`--restart unless-stopped`) plus either the detached loop or a host cron of `run --once`. See [Automate](/sailor/guides/ci) for the full set of options. ## Lifecycle [Section titled “Lifecycle”](#lifecycle) ```bash docker stop agent # stop; project files and state live on the host docker start agent # resume later docker pull sailmoney/sailor && docker rm -f agent && docker run … # upgrade, then `sailor update` ``` # Getting started Two ways in, depending on how you work: * [Quickstart](/sailor/getting-started/quickstart) — install Sailor and go from nothing to a deployed SMA, a registered mandate, and a dispatched transaction, with real commands. * [Operate Sailor with a coding agent](/sailor/getting-started/coding-agent) — the recommended path: scaffold a project, open it in your coding assistant, and let it drive the whole setup. This is Sailor’s edge. Both assume a wallet, an RPC URL for a [supported chain](/sailor/guides/multi-chain), and Node.js 18+. # Operate Sailor with a coding agent Sailor is designed to be operated **through a coding agent** — Claude Code, Cursor, Codex, or any AI coding assistant. You describe the strategy you want; the assistant deploys the SMA, authors and tests the permissions that bound it, signs the mandate, and runs the agent. This is the intended way to use Sailor, and it’s what the scaffold is built for. ## How it works [Section titled “How it works”](#how-it-works) When you run `sailor init`, the scaffold includes an **`AGENTS.md`** operator guide and a set of on-demand **skills** under `.agents/skills/`, following the open [Agent Skills](https://agentskills.io) standard. Assistants that scan skills load each one only when relevant; assistants that don’t follow a routing table in `AGENTS.md` to the same plain-markdown files. It works in Claude Code, Cursor, Copilot, and Codex. A representative slice of the scaffolded skills, by station (the full set of **22** — including one per shared template — is on the [Skills](/sailor/skills) page): | Skill | Station / role | | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `sailor-onboarding` | **Arrive** — set up a new project, or resume a partial one | | `sailor-strategy` | **Strategy** — turn your intent into a concrete spec at `.sail/strategy.md` | | `sailor-mandate-planner` + `sailor-templates` + `sailor-template-*` + `sailor-mandates` | **Mandate** — route the spec to shared templates (one skill per template) or author custom permissions | | `sailor-agent-build` + `sailor-transactions` + `sailor-memory` | **Agent** — build the tick loop, dispatch mechanics, chain-reconciled memory | | `sailor-automation` + `sailor-operate` + `sailor-extend` | **Sail** — run unattended, operate/tune/exit, optional notifications/dashboards | | `sailor-project-info` / `sailor-servers` / `sailor-token-resolve` / `sailor-swap-quote` | Anytime utilities — state, local servers, token/liquidity resolution, swap quotes | ## The flow [Section titled “The flow”](#the-flow) ```bash # scaffold in the current folder npm i @sail.money/sailor ; npx sailor init # or scaffold into a new folder npx @sail.money/sailor init my-agent && cd my-agent && npm install ``` Open the folder in your assistant and say **start**. From there the assistant: 1. **Deploys your SMA** and creates your agent wallet. 2. **Defines your strategy** — it treats `src/` as a blank slate and asks what you want; it does not assume the example code is your strategy. 3. **Builds, tests, and signs your mandate** — authoring permission contracts in the Foundry workspace, running `forge test` and `sailor mandate simulate`, and only then authorizing on-chain. 4. **Runs your agent** — locally with `sailor run`, or on a schedule via the bundled GitHub Actions workflow. 5. **Extends** — notifications, a custom dashboard. ## What the agent is told to do (and not do) [Section titled “What the agent is told to do (and not do)”](#what-the-agent-is-told-to-do-and-not-do) The scaffold’s `AGENTS.md` encodes hard invariants the assistant follows. The ones worth knowing as the operator: * **Owner signing is browser-only.** The assistant never puts your owner key in the terminal — owner approvals happen in the signing-server UI in your browser. * **Setup asks before spending gas; a running agent does not.** Once the mandate is signed, *the mandate is the authorization* — the agent transacts autonomously within it. You are not asked to confirm each dispatch. * **Never authorize a permission before it passes tests.** The assistant must see `forge test` **and** `sailor mandate simulate` pass against samples derived from your strategy before attaching a permission. * **Use the SDK’s signing helpers.** It signs dispatches with `buildDispatchSignature` and detects the kernel’s dispatch model with `detectKernelCapabilities` — never hand-rolled EIP-712, never a hardcoded model. * **Example permissions are not a menu.** The assistant treats shipped templates as [examples to adapt and verify](/sailor/guides/configure-a-template), not audited, drop-in production contracts. * **`approve()` needs explicit coverage.** An ERC-20 `approve` is not covered by a swap/supply/deposit permission — it’s bounded either per-call (separate single dispatches) or as an atomic batch via an `IBatchPermission`. See [Build & register a mandate](/sailor/guides/build-a-mandate). ## Pointing your agent at these docs [Section titled “Pointing your agent at these docs”](#pointing-your-agent-at-these-docs) Coding agents read these docs on your behalf. The simplest path: give your agent the ready-made prompt on [For AI agents](/for-ai-agents), which points it at the docs’ `llms-full.txt` (the full docs as one machine-readable file) and tells it what to do next. A typical instruction: *“Read the Sail docs’ llms.txt, then set up a Sail SMA on Base that lets the agent only swap USDC↔WETH on Uniswap V3 up to 500 USDC per trade.”* The agent pulls the relevant pages, scaffolds, authors a bounded permission, simulates it, and runs. The custody guarantee holds regardless of which assistant you use: capital stays in your Safe, the mandate is enforced on-chain on every dispatch, and you can [pause](/sailor/cli) or revoke at any time. # Quickstart From nothing to a bounded agent dispatching its first transaction. The fastest path is assistant-driven (next page); this page shows the underlying commands so you know what’s happening. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Node.js 18+ * A wallet (MetaMask, Rabby, and others) for the **owner** signing * An RPC URL for a [supported chain](/sailor/guides/multi-chain) — e.g. Ethereum, Base, Arbitrum, Robinhood, or a testnet (12 chains in all; see the full list) * For authoring permissions: [Foundry](https://book.getfoundry.sh) (`forge`) ## 1. Scaffold a project [Section titled “1. Scaffold a project”](#1-scaffold-a-project) Install the package and scaffold — Sailor works with any agent, via **npm** or **Docker**. **npm** ```bash # scaffold in the current folder npm i @sail.money/sailor ; npx sailor init # or scaffold into a new folder npx @sail.money/sailor init my-agent && cd my-agent && npm install ``` **Docker** (no local Node needed) ```bash docker run -d --name agent -P -v "${PWD}:/workspace" sailmoney/sailor ; docker exec agent sailor init ``` `sailor init my-agent` scaffolds into a new `my-agent/` directory (omit the name to scaffold into the current directory). It writes your agent code (`src/`), a Foundry workspace for permission contracts (`contracts/`), a GitHub Actions cron job, and the operator guide (`AGENTS.md`) with its skills. Then open the folder in your coding agent and say **start**. Note **Recommended:** open the folder in Claude Code, Cursor, or Codex and say **“start”** — the scaffolded `AGENTS.md` and its skills drive the entire flow below for you. See [Operate Sailor with a coding agent](/sailor/getting-started/coding-agent). ## 2. Point at a chain [Section titled “2. Point at a chain”](#2-point-at-a-chain) Set an RPC URL and chain in `.sail/.env.local`: ```bash RPC_URL=https://your-endpoint CHAIN_ID=8453 ``` Sailor resolves RPCs from `.sail/.env.local` first (a chain-specific var like `BASE_RPC_URL`, then generic `RPC_URL`), then the shell environment. See [Multi-chain operation](/sailor/guides/multi-chain). ## 3. Generate the agent key and connect your wallet [Section titled “3. Generate the agent key and connect your wallet”](#3-generate-the-agent-key-and-connect-your-wallet) ```bash sailor keys generate # create + encrypt the manager (agent) wallet sailor signer start & # the browser signing daemon (owner signs here) sailor owner connect # open the printed URL, connect your wallet, persist it as owner ``` The **owner** key stays in your browser wallet and is never read by Sailor. The **manager** (agent) key is encrypted on disk at `.sail/keys/manager.json` (geth keystore v3). ## 4. Check feasibility, then deploy an SMA [Section titled “4. Check feasibility, then deploy an SMA”](#4-check-feasibility-then-deploy-an-sma) ```bash sailor capabilities # read-only: chains, kernel model, what you can build — no gas sailor account predict # compute the deterministic SMA address before deploying sailor onboard --new-sma # create the SMA and (optionally) attach a mandate ``` The SMA address is deterministic — the same owner, manager, and salt produce the [same address on every supported chain](/sailor/guides/deploy-sma). ## 5. Author, test, and register a mandate [Section titled “5. Author, test, and register a mandate”](#5-author-test-and-register-a-mandate) Write a permission contract in the scaffolded Foundry workspace (or use an example template), then prove it before authorizing: ```bash forge build sailor mandate simulate --address MyPermission --sma 0xYourSMA # off-chain PASS/FAIL/REVERT, no gas sailor mandate deploy --contract MyPermission --attach --sma 0xYourSMA # deploy + register via the signing UI ``` `mandate simulate` proves the permission accepts the calls you want and rejects the ones you don’t, **before** you spend gas or authorize it on-chain. See [Simulate before going live](/sailor/guides/simulate). **Optional: rehearse it first.** [Shipyard](/sailor/shipyard) forks the real chains onto your own machine with fake money, so you can take the whole journey (deploy, register, configure, run) without spending anything. It needs Foundry, and it keeps its own state entirely separate from `.sail/`. Start it with `sailor sandbox start`. ## 6. Run the agent [Section titled “6. Run the agent”](#6-run-the-agent) ```bash sailor run --once # a single tick — confirm it works sailor run # continuous ``` `sailor run` executes your agent’s `tick()` on a schedule. Successful dispatches are appended to `.sail/activity.jsonl`; reverts are written to stderr. Pause instantly at any time: ```bash sailor session pause # revoke dispatch rights (custody untouched); session resume to restore ``` ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Operate Sailor with a coding agent](/sailor/getting-started/coding-agent) — let your assistant run all of the above. * [Run a strategy & dispatch](/sailor/guides/run-a-strategy) — write the agent `tick()`. * [CLI reference](/sailor/cli) — every command, flag, and default. # Guides Task-oriented walkthroughs, each grounded in real `sailor` commands. In rough order of the operator lifecycle: 1. [Deploy & predict an SMA](/sailor/guides/deploy-sma) — create the account, and compute its deterministic address up front. 2. [Build & register a mandate](/sailor/guides/build-a-mandate) — author permissions, then register them. 3. [Configure a shared template](/sailor/guides/configure-a-template) — use an example template end to end. 4. [Run a strategy & dispatch](/sailor/guides/run-a-strategy) — write the agent `tick()`, run it, and read a denied dispatch. 5. [Simulate before going live](/sailor/guides/simulate) — prove a permission off-chain (PASS / FAIL / REVERT). 6. [Multi-chain operation](/sailor/guides/multi-chain) — the same SMA address across the supported chains. 7. [Automate with GitHub Actions](/sailor/guides/ci) — run the agent on a schedule in CI. Every command here is verified against the Sailor source. Add `--json` to most commands for machine-readable output (useful for agents and CI). # Build & register a mandate A mandate is the set of [permissions](/protocol/permissions) registered on your SMA. With Sailor you author them in a Foundry workspace, prove them off-chain, then register them — every authorizing signature comes from the owner / permission signer in the browser. ## 1. Author a permission [Section titled “1. Author a permission”](#1-author-a-permission) The scaffold includes a Foundry workspace with `@sail/interfaces/IPermission.sol` vendored under `.sail/contracts/`. Write a contract implementing [`IPermission`](/protocol/permissions/ipermission) that encodes your bounds (tokens, amounts, venues, recipients), then build: ```bash forge build ``` See the Protocol’s [Write your first permission](/protocol/guides/write-a-permission) for the contract pattern, and [Configure a shared template](/sailor/guides/configure-a-template) to use an example instead of writing from scratch. ## 2. Simulate before authorizing [Section titled “2. Simulate before authorizing”](#2-simulate-before-authorizing) Never register a permission you haven’t proven. Probe it off-chain (no gas): ```bash sailor mandate simulate --address MyPermission --sma 0xYourSMA \ --target 0xRouter --calldata 0x... --expect pass ``` This `eth_call`s the permission’s `evaluate` with a constructed `Context` and reports PASS / FAIL / REVERT. See [Simulate before going live](/sailor/guides/simulate) for the batch (`--calls`) form and the full option set. ## 3. Deploy and register [Section titled “3. Deploy and register”](#3-deploy-and-register) ```bash # deploy a compiled permission and register it in one flow (owner signs in the browser): sailor mandate deploy --contract MyPermission --attach --sma 0xYourSMA \ --args '["0xPermissionSigner", ["0xTarget"]]' # or register an already-deployed permission: sailor mandate register --address 0xPermission --sma 0xYourSMA ``` `deploy` emits a contract-creation signing request (the owner signs it), reads the deployed address from the receipt, and tracks it in `.sail/state/mandates.json`. `attach` reads the signer nonce, has the owner sign a `RegisterPermission` EIP-712 message, then submits `kernel.registerPermission` with the exact registration fee. Both take `--json` for headless use. Note **The `--args` format.** Constructor args are a JSON array passed to `--args`. Quoting differs by shell — on PowerShell, use `--args-file` instead of inline JSON. See [CLI → Mandate lifecycle](/sailor/cli#mandate-lifecycle) for the exact rules and examples. ## 4. Confirm and sign the mandate [Section titled “4. Confirm and sign the mandate”](#4-confirm-and-sign-the-mandate) ```bash sailor mandate prepare # prepare a mandate draft for review/signing in the UI sailor mandate sign # review and confirm the permissions authorized for your SMA (--yes to skip the prompt) ``` `sign` reconciles against the live on-chain `getPermissions()` before building the payload — permissions revoked on-chain are excluded even if they remain in the local append-only `.sail/state/mandates.json`. The signed result lands in `.sail/mandate.json`, which the runner executes against. ## Critical: ERC-20 approvals need explicit coverage [Section titled “Critical: ERC-20 approvals need explicit coverage”](#critical-erc-20-approvals-need-explicit-coverage) An ERC-20 `approve()` is **not** covered by a swap, supply, or deposit permission — it is a distinct call and needs its own authorization. There are two non-mixable models: * **Per-call (default):** separate single dispatches, each gated by its own `IPermission` — one for the `approve`, one for the action. * **Atomic batch:** one [`IBatchPermission`](/protocol/architecture/dispatch) authorizing the whole `[approve, action, reset]` sequence as a unit. A normal `IPermission` cannot authorize a batch. Choose deliberately; a swap permission alone will not let your agent approve the router. ## Changing the mandate later [Section titled “Changing the mandate later”](#changing-the-mandate-later) ```bash sailor mandate revoke --address 0xPermission --sma 0xYourSMA # or --all sailor mandate update --address MyName --name NewName # tracking metadata only sailor mandate list # permissions deployed from this project ``` Revocation is owner-authorized (`RevokePermissions`) and takes effect in a single block, invalidating the manager’s outstanding pre-signed dispatches. See the Protocol’s [permission lifecycle](/protocol/permissions/lifecycle). # Automate & run unattended Once `sailor run --once` works, you can run the agent on a schedule or as a long-lived loop. The scaffold’s `sailor-automation` skill offers **four options**, by reliability and infra overhead: 1. **GitHub Actions** (cloud runner, zero infra) — the scaffold’s `.github/workflows/agent-tick.yml` runs `sailor run --once` on a cron; `sailor trigger github` fires it on demand. Simplest, but cron timing drifts. 2. **Self-hosted runner** — the same workflow on your own machine for reliable timing. 3. **Docker** — the `sailmoney/sailor` image on any VM or cloud, via a container registry (see [Docker](/sailor/docker)). 4. **Local daemon** — `sailor service install` registers an OS service (launchd / systemd / Task Scheduler) that restarts on crash; `sailor service status`/`stop`/`logs`/`uninstall` manage it. No Docker required. The rest of this page details option 1 (GitHub Actions), the zero-infra default. ## GitHub Actions — one-time setup [Section titled “GitHub Actions — one-time setup”](#github-actions--one-time-setup) **1. Export the CI keystore.** ```bash sailor keys export-ci ``` This copies the **encrypted** agent-wallet keystore to `ci-keystore.json` in the project root and allowlists it in `.gitignore`. The geth v3 keystore is safe to commit — the raw private key is never exposed; it can only be unlocked with the passphrase. **2. Commit the files the runner needs.** ```plaintext ci-keystore.json .sail/account.json .sail/mandate.json ``` **3. Add two repository secrets** (Settings → Secrets and variables → Actions): | Secret | Value | | ----------------- | --------------------------------------------- | | `SAIL_PASSPHRASE` | the passphrase that encrypts the agent wallet | | `RPC_URL` | your RPC endpoint for the agent’s chain | ## How the workflow runs [Section titled “How the workflow runs”](#how-the-workflow-runs) On each scheduled tick the workflow copies `ci-keystore.json` to `.sail/keys/manager.json`, then runs `npx sailor run --once` with `SAIL_PASSPHRASE` set so the manager key is unlocked non-interactively. **No private key ever appears in the workflow file or in the secrets** — only the passphrase and RPC URL do, and the keystore is encrypted. ```yaml # .github/workflows/agent-tick.yml (provided by the scaffold) runs, roughly: # - npm ci # - cp ci-keystore.json .sail/keys/manager.json # - npx sailor run --once # env: SAIL_PASSPHRASE, RPC_URL ``` Fire it manually without waiting for the cron: ```bash sailor trigger github --reason "manual tick" ``` ## Safety notes [Section titled “Safety notes”](#safety-notes) * The agent in CI is still bounded by the on-chain mandate — CI cannot make it exceed its permissions. * You can [pause](/sailor/cli) the session at any time (`sailor session pause`); a paused session makes every scheduled tick a no-op until you resume. * Never commit `SAIL_PASSPHRASE` or any raw private key. Only the **encrypted** `ci-keystore.json` is committed. The `sailor-automation` skill walks through all four options if you’re [operating via a coding agent](/sailor/getting-started/coding-agent). # Configure a shared template Sailor can register a [shared template](/protocol/permissions/shared-templates) — a permission deployed once and configured per account — instead of authoring a permission from scratch. Note **Templates are examples, not the protocol.** The shipped `Shared*` templates and the standalone clone templates are reference implementations that demonstrate the [`IPermission`](/protocol/permissions/ipermission) pattern. They are illustrative, not a fixed part of the trusted core — anyone can deploy their own. You are responsible for the correctness of any permission you register (see [permission correctness is the author’s responsibility](/protocol/security/limitations)). Treat them as starting points to read, adapt, and **verify and test before any production use** — not audited, drop-in contracts. ## The seven shared templates [Section titled “The seven shared templates”](#the-seven-shared-templates) Sail Protocol’s **seven shared permission templates are deployed today** — multi-tenant singletons at the **same CREATE2 address on all 12 supported chains**, registered as `knownTemplates` in the SDK deployment registry. You don’t deploy them; you **register** one on your SMA and **configure** your own bounds: | Template | Bounds | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SwapPermission` | DEX swaps: router + token allowlists, per-tx cap, mandatory oracle slippage band | | `SwapPermissionNoOracle` | Swaps for tokens without an oracle: allowlists + cap + live-pool sanity band | | `BorrowPermission` | Borrowing: protocol + asset allowlist, cap, on-chain LTV check | | `DepositPermission` | Deposits into ERC-4626 vaults / Aave: target + token bounds, cap | | `WithdrawPermission` | Bounded **position exits** — ERC-4626 vault withdraw/redeem and Aave v2/v3 pool withdraw — with proceeds paid **only to the account itself** (target allowlist + per-tx cap) | | `TransferPermission` | Moves ERC-20 tokens the SMA already holds **out** to a pinned recipient: token + recipient allowlists, per-transfer caps | | `ApproveAndCallBatchPermission` | Atomic approve → call → reset-to-zero batches | See what’s live on your chain: ```bash sailor mandate templates # deployed template addresses + how to author your own sailor capabilities # chains, kernel model, mandate templates, strategy primitives (read-only) ``` Their addresses are in the [Protocol → Deployment addresses](/protocol/reference/addresses), and each template has a dedicated [skill](/sailor/skills) that carries its exact parameter schema and the safe order of operations. ## Register, then configure [Section titled “Register, then configure”](#register-then-configure) `sailor mandate register` only **registers** a template — you must also **configure** the per-account bounds: ```bash # 1. Register (owner signs an EIP-712 RegisterPermission in the browser): sailor mandate register --address --sma # 2. Configure your bounds (tokens, caps, venues) for that template: sailor mandate configure --address \ --template SwapPermission --args-file swap-config.json ``` The `sailor-template-*` skills drive this conversationally with the correct schema per template. `--simulate-only` on `configure` gives a gas-free preflight. Note **`sailor mandate deploy-clone` is currently unavailable** — no clone templates are deployed. For a single-account, bespoke permission, author your own `IPermission` and use `sailor mandate deploy --contract --attach` (see [Build & register a mandate](/sailor/guides/build-a-mandate)). ## Always simulate first [Section titled “Always simulate first”](#always-simulate-first) Prove the configured template accepts and rejects the right calls before authorizing: ```bash sailor mandate simulate --address --sma --calls ./samples.json ``` See [Simulate before going live](/sailor/guides/simulate). Then register/confirm exactly as in [Build & register a mandate](/sailor/guides/build-a-mandate). ## WithdrawPermission vs TransferPermission — which one? [Section titled “WithdrawPermission vs TransferPermission — which one?”](#withdrawpermission-vs-transferpermission--which-one) These two are easy to confuse; they do opposite things: * **`WithdrawPermission` exits a position** — it redeems from an ERC-4626 vault or an Aave v2/v3 pool, and the proceeds land **in the SMA itself**. It cannot send funds to any other address. * **`TransferPermission` moves tokens out** — it sends ERC-20 tokens the SMA already holds to a **pinned external recipient**. So: *“exit this vault position”* → `WithdrawPermission`. *“get my money out of the SMA”* / *“send held tokens to my wallet”* → `TransferPermission` (with a one-entry recipient allowlist). An exit and a payout are two permissions, not one. `WithdrawPermission` carries a few non-obvious rules — the `redeem` cap counts shares, not assets; the token allowlist binds only the Aave path; and Compound / Aave v4 are deliberately unsupported. The `sailor-template-withdraw` skill has the exact schema. # Deploy & predict an SMA An SMA is a Safe registered with `SailKernel`, with the kernel enabled as a module. Sailor creates and tracks it in `.sail/account.json`. ## Predict the address first [Section titled “Predict the address first”](#predict-the-address-first) Because the kernel binds the [CREATE2 salt to the account’s principals](/protocol/concepts/deterministic-deployment), you can compute the SMA address before deploying — and it’s identical on every supported chain. ```bash sailor account predict # options: # --owner
owner EOA (defaults to .sail/account.json) # --manager
agent (manager) wallet, mixed into the kernel salt # --salt CREATE2 salt nonce (default: 0) # --chain show the prediction for one chain only # --json machine-readable output ``` The same owner + manager + salt produce the same address everywhere, so you can fund the address ahead of deployment. A different manager or salt lands at a different address (front-run resistance). ## Deploy [Section titled “Deploy”](#deploy) ```bash sailor onboard --new-sma # options: # --sma
use a specific SMA instead of prompting # --new-sma create a new SMA via SailKernel # --salt CREATE2 salt (0 for your first SMA; increment for subsequent ones) # --template also register this permission (kind, label, or address) # --skip-mandate skip the permission-registration step # --json non-interactive JSON output ``` `onboard` walks the full setup: it creates the SMA on-chain (owner signs in the [signing server](/sailor/concepts/keys-and-custody)), optionally attaches a first mandate, and confirms the agent is operational. Owner-signed steps go through the browser; the agent submits what it’s allowed to. ## Deploy the same address on another chain [Section titled “Deploy the same address on another chain”](#deploy-the-same-address-on-another-chain) Once an SMA exists, replicate its address on another supported chain with the same principals: ```bash sailor account deploy-chain --chain 42161 # --chain is required # --salt defaults to the saltNonce stored in .sail/account.json # --json ``` See [Multi-chain operation](/sailor/guides/multi-chain). ## Inspect what you have [Section titled “Inspect what you have”](#inspect-what-you-have) ```bash sailor status # current account, permission, and session status sailor scan # discover the owner's SMAs + permissions + keys → context.json sailor doctor # read-only preflight: kernel model, permission health, RPC + gas balances ``` `doctor` is the right first call before spending gas — it reports the kernel’s [dispatch model](/protocol/architecture/dispatch), permission health, RPC reachability, and gas balances. Next: [Build & register a mandate](/sailor/guides/build-a-mandate). # Multi-chain operation Sailor targets the chains where the Sail trusted core is deployed. Because the core uses [deterministic CREATE2 deployment](/protocol/concepts/deterministic-deployment), the kernel — and your SMA — have the **same address on every supported chain**. ## Supported chains [Section titled “Supported chains”](#supported-chains) | Chain | Chain ID | Native | Type | | ------------ | -------- | ------ | ------- | | Ethereum | 1 | ETH | mainnet | | Base | 8453 | ETH | mainnet | | Arbitrum | 42161 | ETH | mainnet | | Optimism | 10 | ETH | mainnet | | Unichain | 130 | ETH | mainnet | | World Chain | 480 | ETH | mainnet | | MegaETH | 4326 | ETH | mainnet | | Robinhood | 4663 | ETH | mainnet | | BSC | 56 | BNB | mainnet | | HyperEVM | 999 | HYPE | mainnet | | Base Sepolia | 84532 | ETH | testnet | | Eth Sepolia | 11155111 | ETH | testnet | That’s **12 chains** (10 mainnets + 2 testnets). The verified deployment addresses are bundled in the package and exposed via `getSailDeployment(chainId)` in the SDK. The canonical address list is the Protocol’s [Deployment addresses](/protocol/reference/addresses). Inspect them from the CLI: ```bash sailor chains # list supported chains and their SailKernel addresses sailor chains --verify # confirm each kernel is deployed (one eth_getCode per chain) sailor chains --json ``` ## Configuring RPCs per chain [Section titled “Configuring RPCs per chain”](#configuring-rpcs-per-chain) Sailor resolves an RPC URL for a chain in this order (first match wins): a chain-specific var in `.sail/.env.local`, then a generic `RPC_URL` there, then the same two in the shell environment. Two valid patterns: ```bash # Option A — a single active chain RPC_URL=https://your-base-endpoint CHAIN_ID=8453 ``` ```bash # Option B — per-chain (multi-chain projects) CHAIN_ID=8453 BASE_RPC_URL=https://your-base-endpoint ARBITRUM_RPC_URL=https://your-arbitrum-endpoint UNICHAIN_RPC_URL=https://your-unichain-endpoint ETH_MAINNET_RPC_URL=https://your-mainnet-endpoint ROBINHOOD_RPC_URL=https://your-robinhood-endpoint BASE_SEPOLIA_RPC_URL=https://your-base-sepolia-endpoint SEPOLIA_RPC_URL=https://your-sepolia-endpoint ``` Per-chain vars take precedence for their chain, so a multi-chain project resolves each endpoint correctly. `sailor chains --verify` checks every chain that has a configured RPC. Each supported chain has a per-chain env var of the form `_RPC_URL` (e.g. `ROBINHOOD_RPC_URL`) and a built-in default; `sailor chains` lists them. Note **Robinhood (4663)** uses ETH for gas and a default RPC of `https://rpc.mainnet.chain.robinhood.com`; its block explorer is [robinhoodchain.blockscout.com](https://robinhoodchain.blockscout.com), where contracts are not source-verified (the deployment reproduces the canonical addresses via CREATE2 calldata-replay from Base). ## The same SMA on another chain [Section titled “The same SMA on another chain”](#the-same-sma-on-another-chain) Deploy your existing SMA address on an additional chain with the same owner, manager, and salt: ```bash sailor account deploy-chain --chain 42161 # --salt defaults to the saltNonce stored in .sail/account.json ``` Predict first to confirm the address matches across chains: ```bash sailor account predict --chain 42161 ``` Run the agent against a specific chain with `sailor run --chain ` (overrides `CHAIN_ID` and `.env.local` for that run). Note Permission contracts are **per-chain deployments** — registering a permission on Base does not register it on Arbitrum. Author/deploy and register your mandate on each chain you operate. The SMA *address* is shared; its mandate is per-chain. # Run a strategy & dispatch Your strategy is an [`Agent`](/sailor/sdk/agent): a `tick()` that runs on a schedule and returns the dispatches it wants to make. The kernel enforces the mandate on every one. ## Write the tick [Section titled “Write the tick”](#write-the-tick) ```ts import type { Agent, AgentContext, Dispatch } from "@sail.money/sailor/sdk"; const agent: Agent = { name: "dca-usdc-weth", description: "Buys WETH with USDC, capped per trade, once per tick.", async tick(ctx: AgentContext): Promise { const usdc = await ctx.read.balance("0xUSDC"); if (usdc < 100_000_000n) return []; // 100 USDC (6 decimals); skip if low const result = await ctx.client.strategy.swap( ctx.account, { from: "0xUSDC", to: "0xWETH", amount: 100_000_000n, swapPermission: "0xSwapPermission" }, ctx.manager, ); ctx.log(`bought WETH, swap tx ${result.swap.txHash}`); return [result.swap]; }, }; export default agent; ``` `ctx` gives you the SMA address (`ctx.account` / `ctx.safe`), a `SailorClient` (`ctx.client`), a viem `publicClient`, the manager keyring (`ctx.manager`), `ctx.read.{balance, allowance, decimals}`, a `log()` that also appends to `.sail/activity.jsonl`, and a `ctx.data` slot for your own inputs (seed it from a JSON file via the `SAILOR_DATA` env var, or populate it in your agent — Sailor bakes in no third-party data sources). See [the Agent interface](/sailor/sdk/agent). ## Run it [Section titled “Run it”](#run-it) ```bash sailor run --once # a single tick, then exit — confirm it works first sailor run # continuous sailor run --chain 8453 # override CHAIN_ID / .env.local for this run ``` Successful dispatches are appended to `.sail/activity.jsonl`; a reverted transaction is written to stderr as `reverted: (gas used: N)`. ## What a denied dispatch looks like [Section titled “What a denied dispatch looks like”](#what-a-denied-dispatch-looks-like) If the agent tries something the mandate doesn’t allow, the kernel reverts the dispatch — no state changes. The SDK decodes the kernel error so you see *why*: | You’ll see | Meaning | | ------------------------- | -------------------------------------------------------------------------------------------------------- | | `PermissionNotRegistered` | the permission you named isn’t on the SMA’s mandate | | `PermissionDenied` | the permission evaluated the call and returned false (out of bounds, wrong selector/target/amount) | | `SessionInactive` | the session is paused — run `sailor session resume` | | `InvalidManagerSignature` | wrong/stale manager signature (often a lagging RPC node; see [Troubleshooting](/sailor/troubleshooting)) | This is the system working: a denied dispatch means your bounds held. Re-check the call against the permission, or [simulate](/sailor/guides/simulate) it to see PASS/FAIL/REVERT without spending gas. Note Don’t hand-roll the dispatch signature. The SDK’s `buildDispatchSignature` reads the kernel’s on-chain typehash and signs the correct (selective vs conjunctive) struct automatically — you can’t pass the wrong model. `sailor run` and `client.dispatch.*` use it for you. ## Pause and resume [Section titled “Pause and resume”](#pause-and-resume) ```bash sailor session pause # revoke dispatch rights instantly (custody untouched) sailor session resume # re-enable ``` To automate ticks on a schedule without a long-running process, see [Automate with GitHub Actions](/sailor/guides/ci). # Simulate before going live `sailor mandate simulate` probes a permission **off-chain** (`eth_call`, no gas) so you can prove it accepts the calls you want and rejects the ones you don’t — before authorizing it on-chain or running the agent. This is the single most useful safety step in the Sailor workflow. ## Single call [Section titled “Single call”](#single-call) ```bash sailor mandate simulate \ --address MyPermission \ # required: address or tracked name --sma 0xYourSMA \ # probe as this account (ctx.account); defaults to .sail/account.json --target 0xRouter \ # the call target --calldata 0x414bf389... \ # 0x-prefixed calldata --value 0 \ # ETH value in wei (default 0) --expect pass \ # expected outcome: pass | fail (sets a non-zero exit on mismatch) --label "swap usdc->weth" # human-readable label ``` The command builds a `Context` for the named permission and calls its `evaluate` via `eth_call`. It reports one of: * **PASS** — the permission returned true; this call would be allowed. * **FAIL** — the permission returned false; this call would be denied (fail-closed). * **REVERT** — `evaluate` reverted (treated by the kernel as denial). `--expect pass|fail` makes the command exit non-zero on a mismatch, so you can assert behavior in scripts and CI. ## Batch [Section titled “Batch”](#batch) Probe many calls at once with a JSON file: ```bash sailor mandate simulate --address MyPermission --sma 0xYourSMA --calls ./samples.json ``` ```json [ { "target": "0xRouter", "calldata": "0x414bf389...", "value": "0", "expect": "pass", "label": "in-bounds swap" }, { "target": "0xRouter", "calldata": "0x...big...", "expect": "fail", "label": "over the cap" }, { "target": "0xEvil", "calldata": "0x...", "expect": "fail", "label": "wrong target" } ] ``` Each entry takes `target`, `calldata`, optional `value`, `expect`, and `label`. Add `--json` to the command for machine-readable results. Build your samples from the real strategy: include the calls the agent will make (expect pass) **and** the calls it must never make (expect fail). ## Where it fits [Section titled “Where it fits”](#where-it-fits) The rule the scaffold enforces — and you should too — is: **never authorize (attach) a permission before `forge test` and `sailor mandate simulate` both pass** against samples derived from your strategy. Simulation closes the gap between “the contract compiles” and “the contract bounds exactly what I intend.” You can also preview a dispatch through the SDK at runtime with `client.dispatch.preview(...)`, which runs the kernel’s `previewBatch` view and returns `{ approved, reason }`. See [SailorClient](/sailor/sdk/client). # npm package Sailor ships as a single published package — [`@sail.money/sailor`](https://www.npmjs.com/package/@sail.money/sailor) (MIT). It contains **both** the `sailor` CLI and the SDK; the SDK is exposed as a subpath export. There is **no separate `@sail.money/sdk` package** — `@sail.money/sailor` is the one package you install. ## Install & scaffold [Section titled “Install & scaffold”](#install--scaffold) * bash / zsh (macOS, Linux) ```bash # scaffold in the current folder npm i @sail.money/sailor ; npx sailor init # or scaffold into a new folder npx @sail.money/sailor init my-agent && cd my-agent && npm install ``` * PowerShell (Windows) ```powershell # scaffold in the current folder npm i @sail.money/sailor ; npx sailor init # or scaffold into a new folder npx @sail.money/sailor init my-agent ; cd my-agent ; npm install ``` Requires **Node.js ≥ 18**. `sailor init` scaffolds the project from `scaffold/` (agent code in `src/`, a Foundry workspace for custom permissions in `contracts/`, `AGENTS.md`, and the [skills](/sailor/skills)). Then open the folder in your coding agent and say **start**. ## Running the CLI [Section titled “Running the CLI”](#running-the-cli) Inside a scaffolded project the `sailor` binary is available via `npx`: ```bash npx sailor # e.g. npx sailor status, npx sailor run --once ``` Prefer a global command instead: ```bash npm install -g @sail.money/sailor sailor ``` The full command surface is in the [CLI reference](/sailor/cli). ## Using the SDK [Section titled “Using the SDK”](#using-the-sdk) The SDK is imported from the **`@sail.money/sailor/sdk`** subpath — this is what scaffolded agent code uses, and it’s injected as a dependency by `sailor init`: ```ts import { SailorClient, LocalKeyring, getSailDeployment, } from "@sail.money/sailor/sdk"; import type { Agent, AgentContext, Dispatch } from "@sail.money/sailor/sdk"; ``` To add it to another project, install the package and its `viem` peer: ```bash npm install @sail.money/sailor viem ``` Everything — `SailorClient`, the `Agent` interface, EIP-712 builders, template encoders, ABIs, and the deployment/chain registries — imports from that single `@sail.money/sailor/sdk` entry point. See the [SDK reference](/sailor/sdk). Note **One package, no standalone SDK.** Always import the SDK from `@sail.money/sailor/sdk`. A standalone `@sail.money/sdk` distribution with granular subpaths is planned but **not yet published to npm** — do not depend on it; install `@sail.money/sailor`. ## No Node.js? [Section titled “No Node.js?”](#no-nodejs) Run the same CLI from the pre-built container instead — see [Docker](/sailor/docker). # Overview ![](/brand/banner-sailor.jpg) > Turn any coding agent into a money agent. Sailor is the open-source harness for [Sail Protocol](/protocol/protocol): it deploys your SMA, builds your permissions, and runs your strategy on your own machine. Sailor is the operator layer for building and running money agents on Sail Protocol. It ships through the coding agents developers already use (Claude, Cursor, Codex, Gemini, Grok, Hermes, and other compatible agents): from a scaffolded project, it deploys a separately managed account, authors and registers the mandate, and runs the strategy, all locally. No single agent is required. Funds remain in your own **separately managed account (SMA)**. The agent never holds your owner key and acts only through a **mandate** — deterministic on-chain permissions the [protocol](/protocol/protocol) checks on every transaction. That bound is what makes a Sailor agent safe to run with real capital. Note Sailor is the **harness**; [Sail Protocol](/protocol/protocol) is the trusted on-chain core. Sailor guides and operates; the kernel enforces. Sailor targets already-deployed kernels — it never deploys the protocol or holds custody. ## What you can build [Section titled “What you can build”](#what-you-can-build) Autonomous money agents for any use case: * **Trading** — spot, DCA, rebalancing. * **Yield** — lending, borrowing, liquidity providing, staking, looping. * **Payments** — transfers, scheduled moves, operational flows. These are examples, not a boundary. Permissions are arbitrary Solidity, so anything in DeFi can be expressed as a permission and operated by an agent. When no shared template fits, you author your own `IPermission` — see [Build & register a mandate](/sailor/guides/build-a-mandate). ## How it works [Section titled “How it works”](#how-it-works) Open the scaffold in Claude Code, Cursor, Codex, or any AI coding agent and say **start**. The agent walks the journey with you — set up the SMA, define the strategy, build and sign the mandate, write the tick loop, then launch and operate — with a direct CLI equivalent for every step. See [Operate Sailor with a coding agent](/sailor/getting-started/coding-agent), or point your assistant at the docs with the ready-made prompt on [For AI agents](/for-ai-agents). ## Components [Section titled “Components”](#components) | Piece | What it does | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **SDK** (`@sail.money/sailor/sdk`) | `SailorClient`, the `Agent` interface, encrypted keyring, EIP-712 signing, dispatch submission, deployment + chain registries, template encoders. See the [SDK reference](/sailor/sdk). | | **CLI** (`sailor`) | Everything from `sailor init` to `sailor run`: keys, SMA deployment, the full mandate lifecycle, the agent loop, session control, doctor. See the [CLI reference](/sailor/cli). | | **Dashboard** (`sailor ui`) | A local web app for onboarding, wallet gas balances, mandate health, activity, and owner signing — read from the project’s `.sail/`, no hosted backend. See the [Dashboard](/sailor/dashboard). | | **Shipyard** (`sailor sandbox`) | A simulation sandbox that forks the real chains onto your own machine with fake money, against the real deployed contracts, so you can rehearse the whole setup and prove a mandate permits what you think it permits. Real market state, frozen at fork time; fully isolated from live. Needs Foundry. See [Shipyard](/sailor/shipyard). | | **Skills** | **22** curated procedures under `.agents/skills/`, organized by the five stations — onboarding and diagnostics, strategy definition, mandate construction (one skill per shared template plus the full custom-permission lifecycle), agent construction with a verified code skeleton, and unattended operation through exit. The skills *are* the harness: any coding agent reads them natively and follows the same verified path in every project. See [Skills](/sailor/skills). | ## Install [Section titled “Install”](#install) Requires Node.js ≥ 18, or run it from Docker with no Node.js at all. The exact commands are on the [npm package](/sailor/packages) and [Docker](/sailor/docker) pages, and the [Quickstart](/sailor/getting-started/quickstart) walks the whole first run. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Quickstart](/sailor/getting-started/quickstart) — install and go from zero to a dispatched transaction. * [Operate Sailor with a coding agent](/sailor/getting-started/coding-agent) — the flagship workflow: scaffold, open in your assistant, say **start**. * Install and run: [npm package](/sailor/packages) · [Docker](/sailor/docker) · [Skills](/sailor/skills) · [Dashboard](/sailor/dashboard) · [Shipyard](/sailor/shipyard). * Chains: [Deployment addresses](/protocol/reference/addresses) · [Multi-chain operation](/sailor/guides/multi-chain). * Reference: [Concepts](/sailor/concepts) · [Guides](/sailor/guides) · [CLI reference](/sailor/cli) · [SDK reference](/sailor/sdk) · [Security](/sailor/security) · [Troubleshooting](/sailor/troubleshooting). * Community: [Discord](https://discord.gg/9GsxPsHzRv) · [X](https://x.com/SaildotMoney) · [GitHub](https://github.com/sail-money) · [npm](https://www.npmjs.com/package/@sail.money/sailor). Caution The Sail Protocol trusted core and its shared templates **as they stood at the review** were reviewed by [Octane](https://www.octane.security), an AI source-code security scanner, across three analyses; the final analysis found no critical- or high-severity findings. The later `WithdrawPermission` v2 rewrite is **not** covered by it. That review covers the protocol contracts — **not** this harness. A security review is not a guarantee of correctness — do not operate with funds you are not prepared to lose. See [Security](/sailor/security). # SDK reference The Sailor SDK is the TypeScript library underneath the CLI: a high-level `SailorClient`, the `Agent` interface your strategy implements, EIP-712 signing helpers, kernel-capability detection, the contract ABIs, and the deployment + chain registries. It peer-depends on `viem ^2`. * [SailorClient](/sailor/sdk/client) — the full client and its namespaces (`account`, `mandate`, `dispatch`, `strategy`, `session`, `fees`, `principal`). * [The Agent interface](/sailor/sdk/agent) — `Agent` and `AgentContext`: what you implement and what each `tick()` receives. * [Exports & helpers](/sailor/sdk/reference) — the exported functions, types, and ABIs (signing, capability detection, errors, Safe-address math, fees). ## Install & import [Section titled “Install & import”](#install--import) The SDK ships **inside** the `@sail.money/sailor` package and is exposed via a subpath export. In an agent project scaffolded by `sailor init`, it’s already a dependency: ```ts import { SailorClient, buildDispatchSignature, detectKernelCapabilities, LocalKeyring, } from "@sail.money/sailor/sdk"; import type { Agent, AgentContext, Dispatch } from "@sail.money/sailor/sdk"; ``` To add it to another project: ```bash npm install @sail.money/sailor viem ``` The SDK is **only** distributed inside `@sail.money/sailor` — always import it from the `@sail.money/sailor/sdk` subpath. There is no separate `@sail.money/sdk` package; `@sail.money/sailor` is the one package you install. ## How to use it [Section titled “How to use it”](#how-to-use-it) There are two layers, and you’ll usually use both: 1. **`SailorClient`** — a batteries-included client with namespaces for accounts, mandates, dispatch, a swap strategy, sessions, fees, and principal tracking. Read-only methods need only an RPC; state-changing methods take a signer (the manager `LocalKeyring`). Start here. See [SailorClient](/sailor/sdk/client). 2. **The `Agent` interface** — when you run under `sailor run`, you implement `Agent.tick(ctx)` and the runner provides an `AgentContext` (with a ready `SailorClient`, a viem `publicClient`, the manager keyring, balance/allowance helpers, and a logger). See [The Agent interface](/sailor/sdk/agent). Below those sit the primitives — `buildDispatchSignature` (self-detecting EIP-712 dispatch signer), `detectKernelCapabilities` (reads the on-chain dispatch model so you never hardcode it), `LocalKeyring` (the encrypted manager key), the kernel/factory/governance ABIs, and the deployment + chain registries. See [Exports & helpers](/sailor/sdk/reference). Note Always sign dispatches with `buildDispatchSignature` and detect the model with `detectKernelCapabilities` — never hand-roll the EIP-712 struct or hardcode conjunctive vs. selective. The builder reads the kernel’s on-chain typehash and selects the correct struct, so you cannot sign the wrong model. # The Agent interface A Sailor agent is a small object: a name, a description, and a `tick()` that the runner calls on every scheduled execution. `tick()` returns the dispatches it made. ```ts export type Agent = { name: string; description: string; tick(ctx: AgentContext): Promise; }; ``` Export one as the default from your strategy module; `sailor run` imports and ticks it. Keep the financial bounds in the [permission](/sailor/guides/build-a-mandate), and keep timing/selection in `tick()` — see [on-chain vs off-chain](/sailor/concepts/on-chain-off-chain). ## AgentContext [Section titled “AgentContext”](#agentcontext) Every `tick()` receives a fully-wired context: ```ts type AgentContext = { safe: Address; // the SMA address account: Address; // alias of `safe` chainId: number; blockNumber: bigint; timestamp: number; // unix seconds now: Date; // wall-clock time of this tick client: ISailorClient; // a ready SailorClient (see SailorClient) publicClient: PublicClient; // viem client for arbitrary reads (QuoterV2, custom views, multicall) manager: ILocalKeyring; // the manager keyring used to sign dispatches log: (msg: string) => void; // console + appends to .sail/activity.jsonl data: Record; // your own data slot (see below) read: { balance: (token: Address | "native") => Promise; allowance: (token: Address, owner: Address, spender: Address) => Promise; decimals: (token: Address) => Promise; }; }; ``` | Field | Use it for | | ------------------------------------- | ----------------------------------------------------------------- | | `account` / `safe` | the SMA you operate; pass to `client.*` calls | | `client` | high-level dispatch/strategy/mandate operations | | `publicClient` | any on-chain read `ctx.read` doesn’t cover | | `manager` | signing — pass to `client.dispatch.*` / `client.strategy.*` | | `read.balance / allowance / decimals` | quick SMA state reads (`decimals` is cached for the process) | | `log` | activity logging that shows in the dashboard and `activity.jsonl` | | `data` | your inputs (see below) | ### The `data` slot [Section titled “The data slot”](#the-data-slot) Sailor bakes in **no** third-party data sources. `ctx.data` is an open slot for your own: populate it inside your agent, or seed it from a JSON file via the `SAILOR_DATA` environment variable. It defaults to an empty object. Use it for price feeds, signals, or any external input your strategy needs — you own that integration. ## A minimal tick [Section titled “A minimal tick”](#a-minimal-tick) ```ts import type { Agent, AgentContext, Dispatch } from "@sail.money/sailor/sdk"; const agent: Agent = { name: "rebalance-guard", description: "Tops up WETH when the SMA's balance drops below a floor.", async tick(ctx: AgentContext): Promise { const weth = await ctx.read.balance("0xWETH"); if (weth >= 1_000_000_000_000_000_000n) return []; // ≥ 1 WETH, nothing to do const { swap } = await ctx.client.strategy.swap( ctx.account, { from: "0xUSDC", to: "0xWETH", amount: 50_000_000n, swapPermission: "0xSwapPermission" }, ctx.manager, ); ctx.log(`topped up WETH via ${swap.txHash}`); return [swap]; }, }; export default agent; ``` Return the `Dispatch[]` you executed (or `[]` for a no-op tick). Anything outside the mandate is rejected on-chain — see [Run a strategy & dispatch](/sailor/guides/run-a-strategy) for what a denial looks like. # SailorClient `SailorClient` is the high-level entry point. Construct it with an RPC URL and chain id; read-only methods work immediately, and state-changing methods take a manager/permission-signer `LocalKeyring`. ```ts import { SailorClient } from "@sail.money/sailor/sdk"; const client = new SailorClient({ rpcUrl: "https://...", chainId: 8453, kernel: "0x38b508756c976e876EFF05a29E731A4d348BA6ED", // required for on-chain ops mandateFactory: "0x6d2C802ffa0d9A8Ed69A5Bf22c1b63ccB566B8Fc", // for bundled attach flows }); const caps = await client.capabilities(); // detect dispatch model + EIP-712 shape ``` `SailorClientConfig`: `{ rpcUrl: string; chainId: number; kernel?: Address; mandateFactory?: Address }`. Resolve `kernel`/`mandateFactory` from the registry with `getSailDeployment(chainId)` (see [Exports & helpers](/sailor/sdk/reference)). ## Namespaces [Section titled “Namespaces”](#namespaces) The client groups operations into namespaces. ### `client.account` [Section titled “client.account”](#clientaccount) | Method | Description | | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `create(params: CreateAccountParams): Promise` | Deploy a new Safe and register it with the kernel in one transaction (sets `permissionSigner` + `manager`). | | `registerExisting(safe, params): Promise` | Register an existing Safe you control, without redeploying. | | `get(safe): Promise` | Fetch current account state from the kernel. | ### `client.mandate` [Section titled “client.mandate”](#clientmandate) | Method | Description | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `attach(safe, template, params, signer)` | Register a single `IPermission` (signer must be the Safe’s permission signer). | | `attachBatch(safe, items, signer)` | Register multiple permissions atomically (one Safe tx). | | `reconfigure(safe, template, params, signer)` | Update params on a registered permission (re-signs the mandate). | | `replace(safe, oldTemplate, newTemplate, params, signer)` | Atomically replace one permission with another. | | `detach(safe, template, signer)` | Remove a permission from the registered set. | | `deployAndAttachClone(safe, impl, initData, salt, signer)` | Clone an implementation (ERC-1167) and attach it. | | `list(safe): Promise` | All currently registered mandates. | | `isRegistered(safe, permission): Promise` | Whether a permission is registered and active. | | `draft(input: MandateDraftInput): Promise` | Turn a natural-language description into human-readable mandate terms matched against known templates. | ### `client.dispatch` [Section titled “client.dispatch”](#clientdispatch) | Method | Description | | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `single(safe, permission, call, manager, options?)` | Submit one call through `SailKernel.dispatch()`. | | `batch(safe, permission, calls, manager)` | Submit multiple calls as one kernel dispatch (atomic). | | `preview(safe, permission, calls): Promise` | Simulate via the kernel’s `previewBatch` view (no tx); returns `{ approved, calls, reason?, simulation? }`. | `DispatchOptions` (all optional): `nonce` (sign with exactly this manager nonce), `awaitNonce` (wait for a minimum on-chain nonce before signing — handles lagging RPC nodes), `gas` (explicit limit, skips `eth_estimateGas`), `deadline` (defaults to 5 minutes out). Back-to-back `dispatch.single` calls track the nonce automatically. ### `client.strategy` [Section titled “client.strategy”](#clientstrategy) | Method | Description | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `swap(safe, params: SwapParams, manager): Promise` | A delegated token swap via the integrated aggregator (LiFi): fetch a quote, top up the router allowance only if below `amount`, then dispatch the swap. Approve + swap go through `dispatch.single`, so the manager nonce is orchestrated automatically. | `SwapParams`: `from`, `to`, `amount`, optional `slippage` (fraction, default `0.03`), `swapPermission` (required on selective kernels), `approvePermission` (defaults to `swapPermission`), `approveAmount` (defaults to `amount` — approve a larger batch to collapse subsequent buys to swap-only dispatches), `recipient` (defaults to the SMA), `router`. ### `client.session` [Section titled “client.session”](#clientsession) `revoke(safe, signer)` · `activate(safe, signer)` · `status(safe): Promise` — revoke or re-enable the manager’s dispatch rights, or read the current session state. ### `client.fees` [Section titled “client.fees”](#clientfees) `setPolicy(safe, policy: FeePolicy, signer)` · `collect(safe, gross, nav, token, manager): Promise` — set the management/performance fee policy, or trigger collection (gross = AUM value, nav = NAV for the high-water mark). ### `client.principal` [Section titled “client.principal”](#clientprincipal) `recordDeposit(safe, amount, signer)` · `recordWithdrawal(safe, amount, signer)` — record LP deposits/withdrawals in the kernel’s principal ledger (cost basis for performance-fee math). ### `client.capabilities()` [Section titled “client.capabilities()”](#clientcapabilities) Returns `KernelCapabilities` — detects the deployed kernel’s dispatch model (conjunctive vs selective) and EIP-712 shape by reading its on-chain typehash constants. Use this rather than assuming a model. ## Keyrings [Section titled “Keyrings”](#keyrings) State-changing methods take an `ILocalKeyring` (`address`, `sign(hash)`, `signTyped(domain, types, value)`). Load the encrypted manager key with `LocalKeyring`: ```ts import { LocalKeyring } from "@sail.money/sailor/sdk"; const manager = await LocalKeyring.fromKeystoreFile(".sail/keys/manager.json", process.env.SAIL_PASSPHRASE!); ``` See [Exports & helpers](/sailor/sdk/reference) for `LocalKeyring`, the EIP-712 builders, and capability detection. # Exports & helpers The lower-level public surface of `@sail.money/sailor/sdk`, grouped by purpose. Internal and experimental exports are omitted. Types are mirrored from source. ## EIP-712 signing [Section titled “EIP-712 signing”](#eip-712-signing) | Export | Purpose | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `buildDispatchSignature` | Self-detecting `Dispatch` signer — reads the kernel’s on-chain typehash and signs the correct (selective/conjunctive) struct. **Use this; don’t hand-roll.** | | `sailKernelDomain` | The EIP-712 domain for any `SailKernel`. | | `DISPATCH_EIP712_FIELDS` | Typed struct field lists keyed by dispatch model. | | `buildRegisterPermissionTypedData` | Build `RegisterPermission` typed data (accepts `hasDeadline`). | | `buildRegisterPermissionsBatchTypedData` | Batch (`RegisterPermissions`) typed data. | | `signRegisterPermission` | Sign a `RegisterPermission` message. | | `REGISTER_PERMISSION_TYPES`, `REGISTER_PERMISSION_TYPES_NO_DEADLINE`, `REGISTER_PERMISSIONS_BATCH_TYPES` | The EIP-712 type definitions. | ```ts import { buildDispatchSignature, LocalKeyring } from "@sail.money/sailor/sdk"; import { createPublicClient, http } from "viem"; const publicClient = createPublicClient({ transport: http(rpcUrl) }); const manager = await LocalKeyring.fromKeystoreFile(".sail/keys/manager.json", passphrase); const { signature, nonce, deadline, dispatchModel } = await buildDispatchSignature({ publicClient, kernel, chainId: 8453, account: mySafe, permission: myPermission, call: { target: router, value: 0n, data: swapCalldata }, manager, }); // kernel.dispatch(account, permission, target, value, data, signature, deadline) ``` ## Kernel capability detection [Section titled “Kernel capability detection”](#kernel-capability-detection) | Export | Purpose | | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `detectKernelCapabilities` | Read `DISPATCH_TYPEHASH` on-chain; identify conjunctive vs selective. | | `clearCapabilityCache` | Clear the per-process capability cache. | | `DispatchModel`, `KernelCapabilities` | Result types. | | `DISPATCH_TYPE_STRINGS`, `DISPATCH_TYPEHASHES`, `REGISTER_PERMISSION_TYPE_STRINGS`, `REGISTER_PERMISSION_TYPEHASHES` | The known type strings/hashes per model. | ## Kernel errors [Section titled “Kernel errors”](#kernel-errors) | Export | Purpose | | --------------------------------------------- | ---------------------------------------------- | | `decodeKernelError` | Decode a revert into a typed `KernelError`. | | `explainKernelRevert` | Human-readable explanation of a kernel revert. | | `KERNEL_ERROR_ABI`, `KERNEL_ERROR_SIGNATURES` | The error ABI + 4-byte signatures. | ```ts import { explainKernelRevert } from "@sail.money/sailor/sdk"; try { /* dispatch */ } catch (e) { console.error(explainKernelRevert(e)); } ``` ## Deployments & chains [Section titled “Deployments & chains”](#deployments--chains) | Export | Purpose | | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `getSailDeployment(chainId)` | Resolve the kernel / factory / governance addresses for a chain. | | `sailDeployments`, `normalizeDeployment` | The full registry + a normalizer. | | `chains`, `getChain` | The chain registry (`ChainConfig` per chainId). | | `SailChainId`, `SailDeployment`, `KnownTemplate`, `CloneTemplateInfo`, `CloneTemplateParam`, `ChainConfig` | Registry types. | ## Safe-address math [Section titled “Safe-address math”](#safe-address-math) | Export | Purpose | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `computeSailSmaAddress` | Predict an SMA address from principals + salt (the [deterministic address](/protocol/reference/deterministic-addresses)). | | `computeKernelBoundSalt` | Compute `boundSalt = keccak256(saltNonce, caller, permissionSigner, manager, feePolicy)`. | | `computeSafeProxyAddress` | Predict the Safe proxy CREATE2 address. | | `buildSafeSetupInitializer` | Build the Safe `setup` calldata (with the module-enabler delegatecall). | | `encodeSetManager`, `buildSetManagerExecTransaction` | Manager-rotation calldata / Safe exec. | | `buildApprovedHashSignature` | Safe approved-hash signature helper. | | `SAFE_V141` | Canonical Safe v1.4.1 addresses. | | `gnosisSafeAbi`, `gnosisSafeExecAbi`, `safeModuleEnablerAbi`, `safeProxyFactoryAbi` | Safe-side ABIs. | ## Discovery, fees, and swaps [Section titled “Discovery, fees, and swaps”](#discovery-fees-and-swaps) | Export | Purpose | | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | `discoverSafesForOwner`, `getSafeTransactionServiceUrl` | Find an owner’s Safes via the Safe transaction service. | | `estimatePermissionFee` | Estimate the kernel registration fee. | | `fetchLifiQuote`, `minTokenOut`, `encodeApprove`, `LIFI_ROUTERS`, `LIFI_QUOTE_URL`, `DEFAULT_SLIPPAGE` | Aggregator (LiFi) swap primitives behind `client.strategy.swap`. | | `FetchLifiQuoteParams`, `LifiSwapQuote` | Swap quote types. | ## Keyring & ABIs [Section titled “Keyring & ABIs”](#keyring--abis) | Export | Purpose | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `LocalKeyring` (+ `EncryptedKeystore`, `LocalKeyringOptions`) | Load/sign with the encrypted manager key (geth keystore v3). | | `SailKernelAbi`, `MandateFactoryAbi`, `SailGovernanceAbi` | Trusted-core ABIs. | | Signing-server message types | `ClientMessage`, `ServerMessage`, `SigningRequest`, `SigningResponse`, `SigningTxRequest`, `SigningTypedDataRequest`, `SerializedTypedData`, … | ## Core domain types [Section titled “Core domain types”](#core-domain-types) `Account`, `Mandate`, `MandateExplanation`, `Call`, `Dispatch`, `PreviewResult`, `SimulationResult`, `TxResult`, `DispatchOptions`, `SwapParams`, `SwapResult`, `Session`, `FeePolicy`, `PermissionTemplate`, `TemplateEncoder`, `TemplateExplainer`, `MandateItem`, `MandateDraftInput`, `SailorClientConfig`, `CreateAccountParams`, `RegisterAccountParams`, plus the `Agent` / `AgentContext` types ([The Agent interface](/sailor/sdk/agent)). # Security Sailor is **off-chain tooling for an on-chain trust model.** Nothing Sailor does can exceed what the contracts allow — the security guarantees live on-chain in [Sail Protocol](/protocol/security); Sailor makes them operable. This page states Sailor’s own posture honestly; it does not restate the protocol’s guarantees. ## The boundary [Section titled “The boundary”](#the-boundary) Sailor can *propose* and *submit* transactions; only the kernel and the owner-signed mandate decide what *executes*. So a compromised or buggy agent is bounded by contracts it cannot edit, and the worst case off-chain is a **skipped or reverted** transaction — never an out-of-mandate one. See [Sailor & the Protocol](/sailor/concepts/protocol-mapping) and [On-chain vs off-chain](/sailor/concepts/on-chain-off-chain). ## What Sailor does for your safety [Section titled “What Sailor does for your safety”](#what-sailor-does-for-your-safety) * **The kernel evaluates the named permission on every call.** The agent signs a dispatch that names one registered permission; the kernel consults exactly that permission and executes only on `true`. A permission that returns false, reverts, or exceeds its gas cap is a denial — **fail-closed**. * **The owner key is never read by Sailor.** Mandate registration, configuration, and revocation require a deliberate EIP-712 signature from the owner/permission signer in the browser [signing server](/sailor/dashboard) — Sailor only records the owner address and requests signatures. * **The manager (agent) key is encrypted on disk** — geth keystore v3 (scrypt + aes-128-ctr) under `.sail/keys/`, behind a passphrase (or `SAIL_PASSPHRASE` for non-interactive use). It is never transmitted and never baked into the Docker image. * **The session can be paused instantly** — `sailor session pause` (or the dashboard) revokes the agent’s dispatch rights without touching Safe custody; `resume` restores it. Independently, `sailor mandate revoke` removes permissions with an owner signature. * **Off-chain dispatch resolution mirrors the kernel.** Before submitting, Sailor resolves which registered permission authorizes a planned call (including the live registration epoch) and **skips** anything nothing authorizes, logging it to `.sail/activity.jsonl` as `dispatch_denied` — never sending it. * **All addresses are EIP-55 normalized** before any on-chain call or state write. ## Prove the bounds before you run [Section titled “Prove the bounds before you run”](#prove-the-bounds-before-you-run) The fail-closed guarantee is testable before anything is at risk. `sailor mandate simulate` probes a permission’s real `evaluate()` off-chain (an `eth_call` — no gas, no signing) and prints `PASS` / `FAIL` / `REVERT`; `--expect` makes a mismatch exit non-zero, so you can wire it into CI. See [Simulate before going live](/sailor/guides/simulate). ## Reporting & the protocol review [Section titled “Reporting & the protocol review”](#reporting--the-protocol-review) * **Sailor (off-chain harness)** vulnerability reports: **** (see the repo’s [SECURITY.md](https://github.com/sail-money/Sailor/blob/main/SECURITY.md)). * **Smart-contract issues** go to the [protocol’s security policy](https://github.com/sail-money/Protocol/blob/main/SECURITY.md). The Sail Protocol trusted core and its shared permission templates **as they stood at the review** underwent an AI **security review** by [Octane](https://www.octane.security); the final analysis identified no critical- or high-severity findings, and the reports live in the [protocol repo’s docs/security](https://github.com/sail-money/Protocol/tree/main/docs/security) (see [Protocol → Octane security review](/protocol/security/audits)). The `WithdrawPermission` v2 rewrite postdates that review and is **not** covered by it. **That review covers the protocol contracts — not this harness.** A security review is not a guarantee of correctness; size your exposure accordingly. # Shipyard: the simulation sandbox Shipyard runs copies of the real chains on your own machine, with fake money. An agent can be taken all the way through the journey there, deploy an SMA, register and configure a mandate, run the tick loop, without risking anything. The contracts it talks to are the real deployed Sail contracts, so what passes in Shipyard is what the kernel would allow on mainnet. What it gives you is real market **state**, captured at the instant the fork starts: real liquidity, real pool depths, real contract storage, the real deployed kernel. Your own transactions move that state exactly as they would in reality. What does not happen is the market moving underneath you, because nobody else is trading on your fork. So Shipyard is the place to answer “does my mandate actually permit what I think it permits”, and not the place to answer “how does my strategy behave through a drawdown”. See [what it cannot tell you](#what-it-cannot-tell-you). It is entirely optional. Nothing else in Sailor needs it, and a project that never starts it behaves exactly as before. ## What you need first [Section titled “What you need first”](#what-you-need-first) Shipyard forks chains with **anvil**, which ships with [Foundry](https://getfoundry.sh). If Foundry is not installed, `sailor sandbox start` stops immediately and says so. This is the most common reason a first run fails, so install it before anything else: ```bash curl -L https://foundry.paradigm.xyz | bash foundryup ``` `sailor doctor` reports whether anvil was found, alongside its other checks. ## Quickstart [Section titled “Quickstart”](#quickstart) From inside a Sailor project: ```bash sailor sandbox start ``` The command starts a second dashboard, separate from the live one, on its own port, and prints where it is: ```plaintext Sailor Sandbox started at http://localhost:3417 (pid 48231) Stop it with: sailor sandbox stop ``` Open that URL yourself; nothing opens a browser for you. Every page carries a banner reading “Shipyard: simulation sandbox. No real funds.” so there is never a question about which one you are looking at. The onboarding wizard asks which chains to fork, then walks the same setup as the live dashboard. To check on it, or stop it: ```bash sailor sandbox status sailor sandbox stop ``` `stop` shuts down the dashboard and the forks, writing each fork’s chain state to disk first, so the next `sailor sandbox start` resumes the same world rather than forking fresh. Pass `--keep-forks` to stop only the dashboard and leave the forks running. `sailor shipyard` is accepted everywhere `sailor sandbox` is, as an alias. ## What is real and what is not [Section titled “What is real and what is not”](#what-is-real-and-what-is-not) Real: * **The contracts.** The SailKernel, the shared permission templates, and the Safe contracts are the genuinely deployed ones, because the fork carries the chain’s real code. * **Chain state**, copied at the moment the fork starts and frozen there. Pools, balances, and every other contract’s storage are whatever they were on the upstream chain at that block: real liquidity and real depths, not invented numbers. * **Protocol behaviour.** Registration, configuration, dispatch, and permission evaluation run the real code paths. A dispatch the kernel would deny on mainnet is denied here for the same reason. Not real: * **The money.** Balances in Shipyard are written directly into the fork. They exist nowhere else and cannot leave it. * **The wallet.** Shipyard signs with a local development account rather than your own wallet, and a browser wallet extension cannot connect to a Shipyard page. Nothing leaves your machine except the requests that read chain state from your RPC endpoint. There is no hosted service behind any of this. ## What it cannot tell you [Section titled “What it cannot tell you”](#what-it-cannot-tell-you) **Runs are not reproducible between runs.** A fork tracks the latest block at the moment it starts, so two runs of the same agent meet different chain state and can reach different results. Shipyard answers “would this work against the chain as it is now”, not “does this produce the same number every time”. **Only the on-chain boundary is simulated.** Venues whose behaviour depends on state held off chain stay off chain, because there is nothing in the fork to consult. Anything that resolves through an off-chain quote, a relayer, or a private order flow will not behave as it would live. **The market does not move on its own.** A fork mines its own blocks, but nobody else is trading on it, so prices and liquidity sit exactly where they were when the fork started. Measured on a Base fork against the WETH/USDC pool: after 47 blocks with no transactions of ours, the pool’s price and both depths were byte-identical, while the same pool on the real chain moved eight ticks and shed roughly seventeen thousand USDC of depth over the same period. That is what makes Shipyard good at some questions and useless at others: * **It answers:** does this permission block the swap I expect it to block, does my slippage bound hold, does registering and configuring this mandate do what I believe it does, does my agent complete a tick against real liquidity. Your own transactions move the market exactly as they would live, so a trade large enough to move the price still moves it. * **It cannot answer:** how the strategy behaves through a drawdown, across a volatility spike, or against anyone else’s order flow. Nothing arrives to move the price but you. Testing a strategy that reacts to price movement means moving the price yourself. ## Configuring it [Section titled “Configuring it”](#configuring-it) **Which chains.** The onboarding wizard offers the chains Sailor supports and forks each one you select. The fork engine covers a fixed set; picking a chain outside it fails with `Unsupported sandbox chain id`. **How many at once.** The default limit is **3** concurrent forked chains. Each fork is a separate anvil process on its own port, so the limit keeps resource use bounded. Change it from the Shipyard settings panel (the gear in the banner), which stores the value as `maxSandboxChains` in the sandbox’s own `config.json`. The ceiling is the number of chains the fork engine has ports for. **RPC endpoints matter.** A fork reads its state from an upstream RPC. Without a configured endpoint Shipyard falls back to a public one and prints a warning, and those are rate limited hard enough to make forking slow or fail outright. Set a per-chain endpoint in the sandbox’s `.env.local`: ```bash BASE_RPC_URL=https://your-base-endpoint ARBITRUM_RPC_URL=https://your-arbitrum-endpoint ``` Each chain reads its own variable, so a multi-chain Shipyard session resolves each endpoint independently. ## Where state lives [Section titled “Where state lives”](#where-state-lives) Everything Shipyard writes goes under `.shipyard/` in your project, mirroring the shape of `.sail/`: the SMA record, the mandate, the activity log, keys, and `.env.local`. It is git-ignored in full, so none of it can be committed by accident. Expect it to use real disk. Each forked chain’s state is dumped to a file periodically while the dashboard runs, and again on shutdown, so a crash does not lose the session. Those dumps are frequently multiple megabytes each. **Resets are archived, never deleted, and nothing ever prunes them.** Each reset moves the whole world into a new `_reset-backup-/` directory beside it, chain-state dumps included, so you can bring an old world back from the Shipyard settings panel. There is no retention limit and no cleanup on start, stop, or reset: reset often enough and these will fill a disk. They are ordinary directories, so remove the ones you no longer want yourself: ```bash du -sh .shipyard/sandbox/_reset-backup-* # what they are costing you rm -rf .shipyard/sandbox/_reset-backup- ``` Restoring a backup archives the current world first, so a restore consumes one archive and creates another rather than reducing the count. Chain state is saved between runs. Stopping and starting resumes the same world: the SMA you deployed, the mandates you signed, the balances you funded. ## Getting back to live [Section titled “Getting back to live”](#getting-back-to-live) Shipyard and live keep entirely separate state, and this is the single most confusing consequence: **While you are working in Shipyard, the live side looks like an empty project.** `sailor status` and `sailor doctor` read the live root only, so a project you onboarded entirely inside Shipyard reports no SMA and no mandate. That is expected. The work is not lost, it is in the other root. To read the Shipyard side from the terminal, point `SAIL_DIR` at it: ```bash SAIL_DIR=.shipyard/sandbox sailor status ``` To move between the two dashboards, use the “Exit to live dashboard” link in the Shipyard banner, or the Shipyard link on the live dashboard. Each starts the other server if it is not already running. Nothing you did in Shipyard carries over to live: going live means running the real setup against the real chain. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **“anvil was not found on PATH.”** Foundry is not installed, or not on the PATH of the shell running Sailor. Install it as above, then confirm with `anvil --version`. **A fork fails to start, or starts slowly.** Almost always the upstream RPC. Check for the rate-limit warning printed at start, and set a per-chain endpoint as described above. Fork startup retries a few times before giving up, because public endpoints behind load balancers routinely fail the first attempt. **A port is already in use.** Each forked chain uses a fixed port, and the dashboard uses a per-project port distinct from the live dashboard’s. If something else holds one, stop it, or stop a fork you are not using from the Shipyard settings panel. **“This project is already wired to an externally-managed fork.”** A different tool, the standalone Shipyard harness, is already managing forks for this project. It is not the same thing as this feature, and both would run their own fork managers on the same ports. Use that tool’s own dashboard instead, or set `SAILOR_ALLOW_SANDBOX_WITH_WRAP=1` if you understand the collision and want the built-in one anyway. **The dashboard shows an empty project.** Check which one you are looking at. The live dashboard has no banner; the Shipyard one always does. # Skills **Skills are the harness.** Every project `sailor init` scaffolds carries **22** on-demand skills under `.agents/skills/`, following the open [Agent Skills](https://agentskills.io) standard: a slim, always-loaded `AGENTS.md` carries the project map and hard invariants, while the detailed procedures live in the skills, loaded on demand. Any coding agent reads them natively and follows the same verified path in every project. They exist so the safe order of operations is encoded **once** — register → configure → simulate → verify — instead of being re-derived by every agent. Skills are plain markdown; an agent that doesn’t scan skills follows the routing table in `AGENTS.md` to the same files. ## The five stations [Section titled “The five stations”](#the-five-stations) The skills are organized around the five-station journey `AGENTS.md` lays out (each station names its owning skill, entry gate, and exit check): ### 1. ARRIVE [Section titled “1. ARRIVE”](#1-arrive) | Skill | What it does | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sailor-onboarding` | Set up a new (or resume a partial) project — SMA deployment, agent-wallet creation, address prediction, multi-chain deployment. The entry point when you say “start” or “continue”. | ### 2. STRATEGY [Section titled “2. STRATEGY”](#2-strategy) | Skill | What it does | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sailor-strategy` | The guided conversation that turns your intent (“DCA into ETH”, “earn yield on USDC”, “pay contributors weekly”) into a complete, concrete spec at `.sail/strategy.md` — every later station reads it. | ### 3. MANDATE [Section titled “3. MANDATE”](#3-mandate) | Skill | What it does | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sailor-mandate-planner` | Station-3 entry: route each action of the strategy spec to a shared template or to bespoke authoring. | | `sailor-templates` | The registry + reuse guide — which primitives exist and the register → configure reuse flow. | | `sailor-template-*` | **One skill per shared template** (seven — see below). | | `sailor-mandates` | The full custom-`IPermission` lifecycle — designing bounds, authoring Solidity, Foundry testing, deploying, simulating, authorizing, revoke/update/list. | ### 4. AGENT [Section titled “4. AGENT”](#4-agent) | Skill | What it does | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `sailor-agent-build` | Build the agent’s tick loop in `src/agent.ts` from the strategy spec and the registered mandate — from a typecheck-verified skeleton. | | `sailor-transactions` | How dispatches work — the selective dispatch model, signing, batching, permission resolution, and why a transaction is denied or reverted. | | `sailor-memory` | The agent’s append-only, chain-reconciled memory ledger (`.sail/memory/ledger.jsonl`) so a fresh process recovers its own trading history. | ### 5. SAIL [Section titled “5. SAIL”](#5-sail) | Skill | What it does | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `sailor-automation` | Run the agent unattended — four options: GitHub Actions runner, self-hosted runner, Docker on any VM, or a local daemon. | | `sailor-operate` | Operate a live agent — “what did it do”, “why was it denied”, pause, tune caps, widen/narrow the mandate, withdraw funds, exit. | | `sailor-extend` | Optional: extend a live agent with notifications (Telegram, email) and a strategy-specific dashboard. | ### Anytime utilities (not tied to a station) [Section titled “Anytime utilities (not tied to a station)”](#anytime-utilities-not-tied-to-a-station) | Skill | What it does | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `sailor-project-info` | Read-only state — “what’s set up”, “am I ready”, preflight, which chains, what permissions are registered. | | `sailor-servers` | Start / stop / health-check the two local servers — the dashboard and the signing server — plus Docker/remote access. | | `sailor-token-resolve` | Resolve tokens by symbol/address into on-chain metadata **and** a cross-chain, cross-DEX liquidity map. Keyless, no gas. | | `sailor-swap-quote` | Fetch a live Uniswap V3 quote and compute the slippage-adjusted `amountOutMinimum` floor the agent embeds in every swap. | ## One skill per protocol template [Section titled “One skill per protocol template”](#one-skill-per-protocol-template) Each of the protocol’s [seven shared permission templates](/protocol/permissions/shared-templates) has its **own dedicated skill** — it encodes that template’s exact parameter schema, the safe `register → configure → simulate → verify` sequence, and the per-template footguns: | Skill | Template | Gates | | -------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sailor-template-swap` | `SwapPermission` | Oracle-gated DEX swaps: router + token allowlists, per-tx cap, **mandatory** oracle slippage band (large trades) | | `sailor-template-swap-no-oracle` | `SwapPermissionNoOracle` | The default bounded-swap tier: allowlists + cap + live-pool sanity band (not manipulation-resistant) | | `sailor-template-borrow` | `BorrowPermission` | Bounded borrows (Aave/Morpho/Compound) with protocol + asset allowlist, cap, on-chain LTV check | | `sailor-template-deposit` | `DepositPermission` | Deposits into ERC-4626 vaults / Aave with target + token allowlist and a per-tx cap | | `sailor-template-withdraw` | `WithdrawPermission` | Bounded **position exits** — ERC-4626 vault withdraw/redeem and Aave v2/v3 pool withdraw — proceeds paid **only to the account** (target allowlist + per-tx cap) | | `sailor-template-transfer` | `TransferPermission` | ERC-20 transfers within a per-tx cap to a recipient allowlist | | `sailor-template-approve-batch` | `ApproveAndCallBatchPermission` | Atomic approve → call → reset-to-zero batches with token/spender/target/selector allowlists | Templates are **registered and configured through these skills** because the CLI provides the primitives (`sailor mandate register` / `configure` / `simulate`) and the skill is the checklist that sequences them correctly. `sailor mandate register` only **registers** a template — you must also **configure** the per-account bounds. See [Configure a shared template](/sailor/guides/configure-a-template). *** The skills in your scaffold (or in the repo under [`scaffold/.agents/skills/`](https://github.com/sail-money/Sailor/tree/main/scaffold/.agents/skills)) are the authoritative procedures; this page is the map. For the design rationale, see the repo’s [templates-and-skills.md](https://github.com/sail-money/Sailor/blob/main/docs/templates-and-skills.md). # Troubleshooting & FAQ Common issues when operating with Sailor, and what they mean. Most “failures” are the system working — a denied dispatch means your bounds held. ## Dispatches [Section titled “Dispatches”](#dispatches) **My dispatch reverts with `PermissionDenied`.** The named permission evaluated the call and returned false (or reverted) — the call is outside the mandate’s bounds. This is fail-closed behavior, not a bug. [Simulate](/sailor/guides/simulate) the exact call (`sailor mandate simulate`) to see PASS/FAIL/REVERT, then either fix the call or widen the permission deliberately. Common causes: amount over the cap, wrong router/target, wrong recipient, or a selector the permission doesn’t handle. **`PermissionNotRegistered`.** You named a permission address that isn’t on the SMA’s mandate. Check `sailor status` / `sailor mandate list`, and confirm you’re on the right chain — permissions are [per-chain](/sailor/guides/multi-chain). **`SessionInactive`.** The session is paused. Run `sailor session resume`. (Pausing is the intended kill switch — `sailor session pause` — and doesn’t touch custody.) **`InvalidManagerSignature`, but the call looks correct.** Usually a **lagging RPC node** in a load-balanced pool: `eth_estimateGas` or the nonce read hit a node behind the chain tip, producing a stale-nonce signature. Mitigations (in the SDK `DispatchOptions`): pass an explicit `gas` to skip the estimate pre-flight, use `awaitNonce` to wait for the bumped nonce, or set `nonce` explicitly. Back-to-back `dispatch.single` calls already track the nonce automatically. A dedicated RPC endpoint avoids it entirely. **My swap reverts with `CumulativeSlippageTooHigh`.** The aggregator’s default slippage is too tight for a small trade. `client.strategy.swap` defaults `slippage` to `0.03` (3%) for this reason; raise it for very small or illiquid trades. ## Mandates & permissions [Section titled “Mandates & permissions”](#mandates--permissions) **My agent can swap but can’t `approve` the router.** An ERC-20 `approve()` is **not** covered by a swap/supply/deposit permission. Authorize it explicitly — either a separate per-call `IPermission` for the approve, or an atomic [`IBatchPermission`](/protocol/architecture/dispatch) covering the whole `[approve, action, reset]` sequence. A normal `IPermission` cannot authorize a batch. See [Build & register a mandate](/sailor/guides/build-a-mandate). **`mandate deploy --args` fails to parse.** It’s almost always shell quoting. On bash/zsh single-quote the whole JSON array; on **PowerShell use `--args-file`** instead of inline JSON. Numbers and addresses are strings; address arrays are nested arrays; order matches the constructor. See [CLI → Mandate lifecycle](/sailor/cli#mandate-lifecycle). **My permission compiles and tests pass, but should I register it?** Only after `forge test` **and** `sailor mandate simulate` both pass against samples derived from your real strategy — include the calls it must allow *and* the calls it must reject. The shipped templates are [examples to verify and adapt](/sailor/guides/configure-a-template), not audited drop-in contracts; **you** own the correctness of any permission you register (Protocol [limitations](/protocol/security/limitations)). ## Setup & keys [Section titled “Setup & keys”](#setup--keys) **Where does the owner sign? I don’t want my owner key in the terminal.** You don’t — and you shouldn’t. Owner/permission-signer signatures go through the browser [signing server](/sailor/concepts/keys-and-custody): `sailor signer start &`, then `sailor owner connect`. Sailor never reads the owner key. **How do I run headlessly / in CI?** Set `SAIL_PASSPHRASE` to unlock the encrypted manager key, commit the **encrypted** `ci-keystore.json` (via `sailor keys export-ci`), and use the scaffold’s GitHub Actions workflow. Never commit the passphrase or a raw key. See [Automate with GitHub Actions](/sailor/guides/ci). **Which dashboard port?** Each project gets a deterministic port in 3333–3999 derived from its path. Use the URL `sailor ui start` prints, or read `.sail/runtime/ui.json` — don’t assume 3333. ## SDK [Section titled “SDK”](#sdk) **How do I import the SDK?** From the subpath export: `import { SailorClient } from "@sail.money/sailor/sdk"`. It ships inside `@sail.money/sailor` (peer-depends on `viem ^2`) — there is no separate `@sail.money/sdk` package, so install `@sail.money/sailor` and import from `/sdk`. See [SDK reference](/sailor/sdk). **How do I know if the kernel is selective or conjunctive?** Call `detectKernelCapabilities` (or `client.capabilities()`) — it reads the on-chain typehash. Never hardcode the model; sign dispatches with `buildDispatchSignature`, which picks the right struct for you. ## Still stuck? [Section titled “Still stuck?”](#still-stuck) Run `sailor doctor` for a read-only health check (kernel model, permission health, RPC reachability, gas balances). For protocol-level questions — kernel internals, the permission model, fees, governance — see the [Protocol docs](/protocol/protocol). For security disclosure: ****.