@dev.sail.money/sailor 1.3.0-98 → 1.4.0-242

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/AGENTS.md +7 -7
  2. package/LICENSE +1 -1
  3. package/README.md +64 -472
  4. package/package.json +11 -6
  5. package/packages/cli/README.md +1 -1
  6. package/packages/cli/dist/index.cjs +1469 -463
  7. package/packages/cli/dist/server.cjs +19796 -21737
  8. package/packages/sdk/README.md +1 -1
  9. package/packages/sdk/dist/abis/ConfigurablePermission.d.ts +130 -0
  10. package/packages/sdk/dist/abis/ConfigurablePermission.d.ts.map +1 -0
  11. package/packages/sdk/dist/abis/ConfigurablePermission.js +79 -0
  12. package/packages/sdk/dist/abis/ConfigurablePermission.js.map +1 -0
  13. package/packages/sdk/dist/abis/SailKernel.d.ts +42 -0
  14. package/packages/sdk/dist/abis/SailKernel.d.ts.map +1 -1
  15. package/packages/sdk/dist/abis/SailKernel.js +34 -0
  16. package/packages/sdk/dist/abis/SailKernel.js.map +1 -1
  17. package/packages/sdk/dist/abis/index.d.ts +1 -0
  18. package/packages/sdk/dist/abis/index.d.ts.map +1 -1
  19. package/packages/sdk/dist/abis/index.js +1 -0
  20. package/packages/sdk/dist/abis/index.js.map +1 -1
  21. package/packages/sdk/dist/chains.d.ts +0 -7
  22. package/packages/sdk/dist/chains.d.ts.map +1 -1
  23. package/packages/sdk/dist/chains.js +81 -32
  24. package/packages/sdk/dist/chains.js.map +1 -1
  25. package/packages/sdk/dist/client.d.ts +1 -1
  26. package/packages/sdk/dist/client.d.ts.map +1 -1
  27. package/packages/sdk/dist/client.js +137 -16
  28. package/packages/sdk/dist/client.js.map +1 -1
  29. package/packages/sdk/dist/deployments.d.ts +6 -4
  30. package/packages/sdk/dist/deployments.d.ts.map +1 -1
  31. package/packages/sdk/dist/deployments.js +120 -118
  32. package/packages/sdk/dist/deployments.js.map +1 -1
  33. package/packages/sdk/dist/eip712.d.ts +84 -0
  34. package/packages/sdk/dist/eip712.d.ts.map +1 -1
  35. package/packages/sdk/dist/eip712.js +148 -0
  36. package/packages/sdk/dist/eip712.js.map +1 -1
  37. package/packages/sdk/dist/fees.d.ts +2 -1
  38. package/packages/sdk/dist/fees.d.ts.map +1 -1
  39. package/packages/sdk/dist/fees.js +2 -1
  40. package/packages/sdk/dist/fees.js.map +1 -1
  41. package/packages/sdk/dist/index.d.ts +3 -2
  42. package/packages/sdk/dist/index.d.ts.map +1 -1
  43. package/packages/sdk/dist/index.js +3 -2
  44. package/packages/sdk/dist/index.js.map +1 -1
  45. package/packages/sdk/dist/intelligence.d.ts +1 -1
  46. package/packages/sdk/dist/intelligence.js +1 -1
  47. package/packages/sdk/dist/safe.d.ts +49 -0
  48. package/packages/sdk/dist/safe.d.ts.map +1 -1
  49. package/packages/sdk/dist/safe.js +74 -0
  50. package/packages/sdk/dist/safe.js.map +1 -1
  51. package/packages/sdk/package.json +6 -2
  52. package/packages/ui/dist/assets/{add-BX5reKtK.js → add-CBbu2pe_.js} +2 -2
  53. package/packages/ui/dist/assets/{all-wallets-IZ3PlVF-.js → all-wallets-C1oy4NUu.js} +2 -2
  54. package/packages/ui/dist/assets/{app-store-ARgRdMdF.js → app-store-DKqyf0Zv.js} +1 -1
  55. package/packages/ui/dist/assets/{apple-VCaqsI9w.js → apple-DyoIbCFy.js} +2 -2
  56. package/packages/ui/dist/assets/{arrow-bottom-circle-h5vFiZ2I.js → arrow-bottom-circle-CMS-OqZQ.js} +2 -2
  57. package/packages/ui/dist/assets/arrow-bottom-pUWeEqYu.js +8 -0
  58. package/packages/ui/dist/assets/arrow-left-B81HMJNL.js +8 -0
  59. package/packages/ui/dist/assets/arrow-right-Z5czoKlC.js +8 -0
  60. package/packages/ui/dist/assets/arrow-top-C6ym1oll.js +8 -0
  61. package/packages/ui/dist/assets/{bank-6c28yOM9.js → bank-B41nlslf.js} +2 -2
  62. package/packages/ui/dist/assets/{basic-CJnbTfSd.js → basic-CWqOb7CV.js} +177 -177
  63. package/packages/ui/dist/assets/{browser-CDKlxM3D.js → browser-DDKdqoZ9.js} +2 -2
  64. package/packages/ui/dist/assets/{card-DQdED02q.js → card-BgOmTajq.js} +2 -2
  65. package/packages/ui/dist/assets/ccip-CtJUKWRd.js +1 -0
  66. package/packages/ui/dist/assets/{checkmark-JoRNVoFV.js → checkmark-CGqQipH4.js} +2 -2
  67. package/packages/ui/dist/assets/{checkmark-bold-B6bGHVIs.js → checkmark-bold-Ci5xf90j.js} +2 -2
  68. package/packages/ui/dist/assets/chevron-bottom-D7Xert17.js +8 -0
  69. package/packages/ui/dist/assets/chevron-left-BGbUebR2.js +8 -0
  70. package/packages/ui/dist/assets/chevron-right-xVsESnHU.js +8 -0
  71. package/packages/ui/dist/assets/chevron-top-DEA9cR6v.js +8 -0
  72. package/packages/ui/dist/assets/{chrome-store-D-fiJ9qJ.js → chrome-store-BsyI-4PI.js} +2 -2
  73. package/packages/ui/dist/assets/{clock-xCPkzwjj.js → clock-Dct9oQ3M.js} +2 -2
  74. package/packages/ui/dist/assets/{close-BkqmPpWG.js → close-BvSCnI2y.js} +2 -2
  75. package/packages/ui/dist/assets/{coinPlaceholder-PIvN9oq_.js → coinPlaceholder-p7RiWkon.js} +2 -2
  76. package/packages/ui/dist/assets/{compass-CW5kWcxW.js → compass-BUeeZqX4.js} +2 -2
  77. package/packages/ui/dist/assets/{copy-DUZlAQJJ.js → copy-C5jXbxLq.js} +2 -2
  78. package/packages/ui/dist/assets/core-CiAm42Mq.js +907 -0
  79. package/packages/ui/dist/assets/cursor-C0T7JWFO.js +3 -0
  80. package/packages/ui/dist/assets/{cursor-transparent-BUAMB0G1.js → cursor-transparent-BZp3-JmA.js} +2 -2
  81. package/packages/ui/dist/assets/{desktop-BxdlwV8P.js → desktop-BIui_Lzb.js} +2 -2
  82. package/packages/ui/dist/assets/{disconnect-GelNh3mn.js → disconnect-B3Bxdchp.js} +2 -2
  83. package/packages/ui/dist/assets/{discord-N0jnxkzC.js → discord-VWI0yDyx.js} +2 -2
  84. package/packages/ui/dist/assets/{etherscan-B41u7wF4.js → etherscan-BpBvUT9i.js} +2 -2
  85. package/packages/ui/dist/assets/events-DQ172AOg.js +1 -0
  86. package/packages/ui/dist/assets/{exclamation-triangle-BWJC56FC.js → exclamation-triangle-o1TJHIUw.js} +2 -2
  87. package/packages/ui/dist/assets/{extension-G6I6oogw.js → extension-4RaLvbh5.js} +2 -2
  88. package/packages/ui/dist/assets/external-link-BHIwjDgj.js +8 -0
  89. package/packages/ui/dist/assets/{facebook-47twy32b.js → facebook-Bes5g-wN.js} +2 -2
  90. package/packages/ui/dist/assets/{farcaster-DYqnqCqs.js → farcaster-CiJ-352m.js} +2 -2
  91. package/packages/ui/dist/assets/{filters-dF6xeJnE.js → filters-DMOteumb.js} +2 -2
  92. package/packages/ui/dist/assets/{github-DbW4Ihz2.js → github-D2nDec9X.js} +2 -2
  93. package/packages/ui/dist/assets/{google-TKm7G8LF.js → google-lZZlQZPQ.js} +2 -2
  94. package/packages/ui/dist/assets/{help-circle-CY3qTmsG.js → help-circle-D4cSSziR.js} +1 -1
  95. package/packages/ui/dist/assets/{id-JpHW4q14.js → id-BmhXZq5P.js} +2 -2
  96. package/packages/ui/dist/assets/{image-Dip2fG98.js → image-Dz4FPeET.js} +2 -2
  97. package/packages/ui/dist/assets/index-B07bCKJ8.css +1 -0
  98. package/packages/ui/dist/assets/index-BV8s6ez1.js +1 -0
  99. package/packages/ui/dist/assets/index-BeUp3nOq.js +1 -0
  100. package/packages/ui/dist/assets/index-CvRylmZJ.js +16 -0
  101. package/packages/ui/dist/assets/{index-q0mGElxZ.js → index-DeoQO45V.js} +3 -3
  102. package/packages/ui/dist/assets/index-Et5S3ZaW.js +47 -0
  103. package/packages/ui/dist/assets/index-LiKeH8Cd.js +1780 -0
  104. package/packages/ui/dist/assets/index.es-dfpEui-9.js +26 -0
  105. package/packages/ui/dist/assets/{info-B1enFsqh.js → info-77RSvIMg.js} +2 -2
  106. package/packages/ui/dist/assets/{info-circle-D4j3FISs.js → info-circle-CWsII-NN.js} +2 -2
  107. package/packages/ui/dist/assets/{lightbulb-DZbKr-vS.js → lightbulb-BZXzm7XE.js} +2 -2
  108. package/packages/ui/dist/assets/{mail-D-TO_uVQ.js → mail-DMG1YqB0.js} +2 -2
  109. package/packages/ui/dist/assets/metamask-sdk-K1x_iqcc.js +542 -0
  110. package/packages/ui/dist/assets/{mobile-Bkmwzav2.js → mobile-DuJfOBSk.js} +2 -2
  111. package/packages/ui/dist/assets/{more-D1_I_jiB.js → more-Cs9ySK2F.js} +2 -2
  112. package/packages/ui/dist/assets/{network-placeholder-DUaFjoXh.js → network-placeholder-CMDPmB1E.js} +2 -2
  113. package/packages/ui/dist/assets/{nftPlaceholder-Cc0iUryh.js → nftPlaceholder-Cj0f9y2u.js} +2 -2
  114. package/packages/ui/dist/assets/{off-BvX71sse.js → off-9zSSsm-e.js} +2 -2
  115. package/packages/ui/dist/assets/{play-store-XzLbtbTd.js → play-store-CPwIb8Gj.js} +2 -2
  116. package/packages/ui/dist/assets/{plus-B77_RmYR.js → plus-BOHQri8o.js} +2 -2
  117. package/packages/ui/dist/assets/{qr-code-BNeIhB1d.js → qr-code-DqhbK276.js} +1 -1
  118. package/packages/ui/dist/assets/{recycle-horizontal-B9sX-yty.js → recycle-horizontal-BpGM56Qz.js} +2 -2
  119. package/packages/ui/dist/assets/{refresh-BWRygnjm.js → refresh-BPzZlhTP.js} +2 -2
  120. package/packages/ui/dist/assets/{reown-logo-DCAEsGFm.js → reown-logo-OCkI0xzo.js} +2 -2
  121. package/packages/ui/dist/assets/{search-ZXIqrawS.js → search-B1JEJvwq.js} +2 -2
  122. package/packages/ui/dist/assets/{secp256k1-Doz05h4g.js → secp256k1-BqUeV9Cb.js} +1 -1
  123. package/packages/ui/dist/assets/{send-BQcO0jNC.js → send-JNsDlNoq.js} +2 -2
  124. package/packages/ui/dist/assets/{swapHorizontal-4yZEzz2K.js → swapHorizontal-C3Z1FImK.js} +2 -2
  125. package/packages/ui/dist/assets/{swapHorizontalBold-DxBKu3sl.js → swapHorizontalBold-Cmdy2iAn.js} +2 -2
  126. package/packages/ui/dist/assets/{swapHorizontalMedium-DAQYJ2JK.js → swapHorizontalMedium-DFX57Nn9.js} +2 -2
  127. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-CUlZzFJ6.js → swapHorizontalRoundedBold-baB389c8.js} +2 -2
  128. package/packages/ui/dist/assets/{swapVertical-CjWHAaDM.js → swapVertical-DmAYGfK1.js} +2 -2
  129. package/packages/ui/dist/assets/{telegram-JBeabiJP.js → telegram-BHbYE-nI.js} +2 -2
  130. package/packages/ui/dist/assets/{three-dots-D3R8OjSx.js → three-dots-BwRWl6qJ.js} +2 -2
  131. package/packages/ui/dist/assets/{twitch-1f9z7xVO.js → twitch-Drx0q6B6.js} +2 -2
  132. package/packages/ui/dist/assets/{twitterIcon-BW7n2xnp.js → twitterIcon-AuJ2KQ6p.js} +2 -2
  133. package/packages/ui/dist/assets/{verify-Cs5owI38.js → verify-Dkrd3q7o.js} +2 -2
  134. package/packages/ui/dist/assets/{verify-filled-Ci5sx5qS.js → verify-filled-B9e3-nz-.js} +2 -2
  135. package/packages/ui/dist/assets/{w3m-modal-DnWtss8u.js → w3m-modal-DtkEjQA7.js} +1 -1
  136. package/packages/ui/dist/assets/{wallet-MaGNqaW-.js → wallet-CqYzSiBd.js} +2 -2
  137. package/packages/ui/dist/assets/{wallet-placeholder-kTOy12_L.js → wallet-placeholder-B05kTN_K.js} +2 -2
  138. package/packages/ui/dist/assets/{walletconnect-CyuP92jg.js → walletconnect-BwoPXLaS.js} +3 -3
  139. package/packages/ui/dist/assets/{warning-circle-BImUCdpG.js → warning-circle-DK9Ai8rT.js} +2 -2
  140. package/packages/ui/dist/assets/{x-II45JHpV.js → x-BNdh5DVW.js} +2 -2
  141. package/packages/ui/dist/index.html +2 -2
  142. package/templates/default/.agents/skills/sail-mandates/SKILL.md +1 -1
  143. package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
  144. package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +3 -3
  145. package/templates/default/.agents/skills/sail-onboarding/SKILL.md +34 -33
  146. package/templates/default/.agents/skills/sail-servers/SKILL.md +1 -0
  147. package/templates/default/.agents/skills/sail-swap-quote/SKILL.md +76 -0
  148. package/templates/default/.agents/skills/sail-template-approve-batch/SKILL.md +98 -0
  149. package/templates/default/.agents/skills/sail-template-borrow/SKILL.md +68 -0
  150. package/templates/default/.agents/skills/sail-template-deposit/SKILL.md +61 -0
  151. package/templates/default/.agents/skills/sail-template-swap/SKILL.md +207 -0
  152. package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +100 -0
  153. package/templates/default/.agents/skills/sail-template-transfer/SKILL.md +52 -0
  154. package/templates/default/.agents/skills/sail-template-withdraw/SKILL.md +50 -0
  155. package/templates/default/.agents/skills/sail-templates/SKILL.md +142 -0
  156. package/templates/default/.agents/skills/sail-templates/catalog.mjs +200 -0
  157. package/templates/default/.agents/skills/sail-templates/deployed.json +133 -0
  158. package/templates/default/.agents/skills/sail-templates/oracles/IOracle.sol +13 -0
  159. package/templates/default/.agents/skills/sail-templates/oracles/UniV3TwapOracle.sol +114 -0
  160. package/templates/default/.agents/skills/sail-templates/oracles/libs/FullMath.sol +62 -0
  161. package/templates/default/.agents/skills/sail-templates/oracles/libs/TickMath.sol +44 -0
  162. package/templates/default/.agents/skills/sail-templates/references/config-schemas.md +139 -0
  163. package/templates/default/.agents/skills/sail-templates/references/reuse-flow.md +139 -0
  164. package/templates/default/.agents/skills/sail-token-resolve/SKILL.md +174 -0
  165. package/templates/default/.env.example +9 -0
  166. package/templates/default/AGENTS.md +107 -13
  167. package/templates/default/docs/PERMISSION_MODEL.md +2 -2
  168. package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +4 -0
  169. package/{examples → templates/default/examples}/custom-mandate/README.md +4 -1
  170. package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +2 -0
  171. package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +4 -0
  172. package/templates/default/scripts/quote-swap.mjs +211 -0
  173. package/templates/default/scripts/resolve-token.mjs +992 -0
  174. package/templates/default/scripts/shared-template-addr.mjs +110 -0
  175. package/templates/default/test/BoundedCallPermission.t.sol +2 -1
  176. package/docs/PERMISSION_MODEL.md +0 -93
  177. package/examples/README.md +0 -24
  178. package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
  179. package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
  180. package/examples/lifi-permissions/README.md +0 -53
  181. package/packages/ui/dist/assets/arrow-bottom-Cwptw8fW.js +0 -8
  182. package/packages/ui/dist/assets/arrow-left-bM1TRcV9.js +0 -8
  183. package/packages/ui/dist/assets/arrow-right-BYd_N4Jr.js +0 -8
  184. package/packages/ui/dist/assets/arrow-top-CLUnwVE2.js +0 -8
  185. package/packages/ui/dist/assets/ccip-BL7RIPfY.js +0 -1
  186. package/packages/ui/dist/assets/chevron-bottom-BMCelIEu.js +0 -8
  187. package/packages/ui/dist/assets/chevron-left-ryPLhHCc.js +0 -8
  188. package/packages/ui/dist/assets/chevron-right-CdDNRv7f.js +0 -8
  189. package/packages/ui/dist/assets/chevron-top-BxWIjjGk.js +0 -8
  190. package/packages/ui/dist/assets/core-von2c1sc.js +0 -907
  191. package/packages/ui/dist/assets/cursor-BzRVY7BD.js +0 -3
  192. package/packages/ui/dist/assets/events-C9icG2YB.js +0 -1
  193. package/packages/ui/dist/assets/external-link-BPaGQf4a.js +0 -8
  194. package/packages/ui/dist/assets/fallback-BTfJeGjh.js +0 -1
  195. package/packages/ui/dist/assets/index-BGJ_8fMk.js +0 -1
  196. package/packages/ui/dist/assets/index-Bq3PAopp.js +0 -16
  197. package/packages/ui/dist/assets/index-C4_cA5XW.js +0 -1
  198. package/packages/ui/dist/assets/index-CaDN0gL4.js +0 -1
  199. package/packages/ui/dist/assets/index-DLElIcBr.js +0 -1789
  200. package/packages/ui/dist/assets/index-lhNfD4jA.css +0 -1
  201. package/packages/ui/dist/assets/index.es-BaCKS_tW.js +0 -26
  202. package/packages/ui/dist/assets/metamask-sdk-DljW3QD1.js +0 -542
  203. package/packages/ui/dist/assets/native-CJ5et6AR.js +0 -1
  204. package/packages/ui/dist/assets/parseSignature-BAS-wKoj.js +0 -1
  205. package/scripts/check-docs.mjs +0 -309
  206. package/scripts/check-init.mjs +0 -123
  207. package/scripts/check-update.mjs +0 -177
  208. package/scripts/clean.mjs +0 -17
  209. package/scripts/pack.mjs +0 -21
  210. package/templates/default/.agents/skills/sail-overview/SKILL.md +0 -100
  211. package/templates/default/.agents/skills/sail-sailor/SKILL.md +0 -190
  212. package/templates/default/.agents/skills/sail-setup/SKILL.md +0 -38
  213. package/templates/default/.gitlab-ci.yml +0 -58
  214. /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
  215. /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
  216. /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
  217. /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
  218. /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
  219. /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
  220. /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
  221. /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
  222. /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
  223. /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
  224. /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
  225. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
  226. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
  227. /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
  228. /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
  229. /package/{examples → templates/default/examples}/permissions/README.md +0 -0
  230. /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
  231. /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
@@ -0,0 +1,139 @@
1
+ # Config schemas & enforced invariants — authoritative (from source)
2
+
3
+ Each shared template stores per-account config under `mapping(address => …)` and decodes a
4
+ `configure(...)` blob in `_applyConfig`. The tuples below are taken **directly from
5
+ `Protocol/contracts/templates/*.sol`** and are the source of truth — encode with
6
+ `abi.encode(...)` in that exact order. (The `@sail/sdk/templates` builders track a
7
+ previously-deployed set; verify a builder's params match the tuple here before using it.)
8
+
9
+ All caps/amounts are in the relevant token's **base units**. Every template runs under the
10
+ kernel's `staticcall` + gas cap + fail-closed semantics: a revert ⇒ deny.
11
+
12
+ ---
13
+
14
+ ## SwapPermission
15
+ ```
16
+ abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
17
+ uint256 maxAmountPerTx, uint256 maxSlippageBps,
18
+ address priceOracle, uint256 maxPriceAgeSec)
19
+ ```
20
+ Selectors: `0x414bf389` (V3 `exactInputSingle` w/ deadline), `0x04e45aaf` (V3-02 no deadline),
21
+ `0x38ed1739` (V2 `swapExactTokensForTokens`). Invariants: `value == 0` (native value rejected);
22
+ `target ∈ routers`; **`tokenIn != tokenOut` / `path[0] != path[last]`** (self-routes denied — a
23
+ round-trip would burn AMM fees while an oracle reporting base==quote clears the band);
24
+ `tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last] ∈ tokensOut`;
25
+ `recipient`/`to == account`; `amountIn ≤ maxAmountPerTx`; **oracle slippage band ALWAYS enforced**.
26
+ The oracle is **mandatory** (contract `v2`): `_applyConfig` reverts `OracleRequired` if
27
+ `priceOracle == 0`, `MissingPriceAge` if `maxPriceAgeSec == 0`, and `SlippageBpsTooLarge` if
28
+ `maxSlippageBps > 9_999`. `maxSlippageBps == 0` ⇒ zero tolerance (strictest), NOT a bypass.
29
+ `priceOracle` is an `IOracle` adapter (`getPrice(base,quote) → (price, dec, updatedAt)`), not a raw
30
+ feed; dust trades whose floor truncates to 0 are denied. V2 intermediate hops are NOT checked.
31
+ (For no-oracle tokens use `SwapPermissionNoOracle`.)
32
+
33
+ ## SwapPermissionNoOracle
34
+ ```
35
+ abi.encode(address[] routers, address[] tokensIn, address[] tokensOut,
36
+ uint256 maxAmountPerTx, ReferencePool[] referencePools)
37
+
38
+ struct ReferencePool { address tokenIn; address tokenOut; address pool;
39
+ PoolKind kind /* 0=V2, 1=V3 */; uint256 toleranceBps; }
40
+ ```
41
+ Same selectors and structural checks as `SwapPermission` (`0x414bf389`, `0x04e45aaf`,
42
+ `0x38ed1739`): `target ∈ routers`; `tokenIn`/`path[0] ∈ tokensIn`; `tokenOut`/`path[last] ∈
43
+ tokensOut`; `recipient`/`to == account`; `amountIn ≤ maxAmountPerTx`. **Differs** from
44
+ `SwapPermission` in the price judgement: instead of an oracle band it enforces a **pool-referenced
45
+ hallucination band** — `amountOutMin` must be ≥ the output implied by the live spot price of the
46
+ operator-named `referencePool` for that directional pair, minus `toleranceBps`, and `amountOutMin
47
+ > 0`. Config-time invariants (`_applyConfig` reverts ⇒ configure fails): `toleranceBps ≤ 5_000`
48
+ (50% max); `pool != 0`; pool's `token0()`/`token1()` must match the pair (orientation precomputed);
49
+ **strict coverage** — every non-self directional `(tokenIn, tokenOut)` combination MUST have a
50
+ reference pool or configure reverts `MissingReferencePool`. Fail-closed at evaluate if the pool is
51
+ missing/unreadable/illiquid or the floor truncates to zero.
52
+
53
+ > ⚠️ **NOT manipulation-resistant.** The reference is a single pool's LIVE spot price, movable
54
+ > within the same transaction (flash-loan / sandwich). This band only catches an *honest* mistake
55
+ > (a confused agent quoting a wildly wrong price); it is NOT slippage protection. For
56
+ > manipulation-resistant protection use the oracle-gated `SwapPermission`. Cap is per-tx, not
57
+ > cumulative.
58
+
59
+ ## BorrowPermission
60
+ ```
61
+ abi.encode(address[] protocols, address[] assets, uint256 maxAmountPerTx,
62
+ uint256 maxLtvBps, address collateralOracle, address borrowOracle,
63
+ uint256 maxPriceAgeSec)
64
+ ```
65
+ Selectors: Aave `borrow(address,uint256,uint256,uint16,address)` (variable-rate only —
66
+ `rateMode != 2` denies; stable-rate debt is rejected), Morpho **Optimizer/Morpho-Aave**
67
+ `borrow(address,uint256,address,address)` (NOT Morpho Blue — its ABI differs and simply won't
68
+ match, i.e. fails closed), Compound `borrow(uint256)` (target is the cToken; the underlying is
69
+ resolved via `cToken.underlying()` and both the allowlist and LTV are underlying-denominated —
70
+ targets with no `underlying()`, e.g. cETH, are denied). Invariants: `protocols`/`assets` must be
71
+ non-empty with no zero addresses (`EmptyAllowlist`/`ZeroAddress` revert otherwise);
72
+ `target ∈ protocols`; `asset ∈ assets`; `amount ≤ maxAmountPerTx`; `onBehalfOf`/`receiver ==
73
+ account`. **Oracle modes:** zero oracles ⇒ amount-cap-only (no LTV ceiling applied at all, despite
74
+ `maxLtvBps` being stored); both oracles set ⇒ `_ltvCheck` enforces the LTV bound. Exactly one
75
+ oracle set reverts `OracleConfigInconsistent` at configure — it's an all-or-nothing pair.
76
+
77
+ ## TransferPermission
78
+ ```
79
+ abi.encode(address[] allowedRecipients, address[] allowedTokens, uint256 maxAmountPerTx)
80
+ ```
81
+ Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
82
+ `allowedRecipients`/`allowedTokens` must be non-empty with no zero addresses
83
+ (`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ allowedTokens`;
84
+ `to ∈ allowedRecipients`; `amount ≤ maxAmountPerTx`;
85
+ **`transferFrom` requires `from == account`** (stricter than the old TransferTarget).
86
+ Max 50 entries per allowlist.
87
+
88
+ ## DepositPermission
89
+ ```
90
+ abi.encode(address[] targets, address[] tokens, uint256 maxAmountPerTx)
91
+ ```
92
+ Selectors: `deposit(uint256,address)` (ERC-4626), `mint(uint256,address)`,
93
+ `deposit(address,uint256,address,uint16)` (Aave v2), `supply(address,uint256,address,uint16)`
94
+ (Aave v3). Invariants: `value == 0`; `targets`/`tokens` must be non-empty with no zero addresses
95
+ (`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target ∈ targets`; token
96
+ allowlist enforced (Aave: the `asset` arg; ERC-4626: `target` itself must be in `tokens`);
97
+ `amount`/`shares ≤ maxAmountPerTx`; `receiver`/`onBehalfOf == account`. `mint` cap is in
98
+ **shares** — account for share price.
99
+
100
+ ## WithdrawPermission
101
+ ```
102
+ abi.encode(address[] tokens, address allowedRecipient, uint256 maxAmountPerTx)
103
+ ```
104
+ Selectors: `0xa9059cbb` `transfer`, `0x23b872dd` `transferFrom`. Invariants: `value == 0`;
105
+ `tokens` must be non-empty and `allowedRecipient` non-zero, with no zero-address tokens
106
+ (`EmptyAllowlist`/`ZeroAddress` revert at configure otherwise); `target (token) ∈ tokens`;
107
+ `to == allowedRecipient` (single address per config); `amount ≤ maxAmountPerTx`;
108
+ **`transferFrom` requires `from == account`**. To change the recipient, `reconfigure` with a new
109
+ blob.
110
+
111
+ ## ApproveAndCallBatchPermission
112
+ ```
113
+ abi.encode(Config{
114
+ address[] tokens; address[] spenders; ConsumingPair[] consumingPairs;
115
+ uint256[] maxApprovalAmounts; bool requireAmountMatch; bool allowUnconstrainedRecipient;
116
+ })
117
+ struct ConsumingPair { address target; bytes4 selector; }
118
+ ```
119
+ ABI tuple: `(address[],address[],(address,bytes4)[],uint256[],bool,bool)`. `consumingPairs` is a
120
+ **struct array**, not two flat arrays — each `(target, selector)` is bound together, so a selector
121
+ is authorised only on its paired target. Authorises exactly the 3-call batch:
122
+ `approve(spender, amount)` → consuming call → `approve(spender, 0)`. Invariants: `tokens`,
123
+ `spenders`, and `consumingPairs` must each be non-empty and contain no zero addresses/selectors
124
+ (`EmptyAllowlist` reverts otherwise); token ∈ `tokens` with `amount ≤ maxApprovalAmount[token]`;
125
+ `spender ∈ spenders`; `(target, selector)` ∈ `consumingPairs`; the pre-batch allowance on
126
+ `(token, spender)` must be zero; the consuming call must target the approved `spender` and pull the
127
+ approved `token`; allowance reset to zero in the same batch; if `requireAmountMatch`, the consuming
128
+ call's leading `uint256` arg must equal the approve amount. `maxApprovalAmounts` is index-parallel
129
+ with `tokens` (length mismatch reverts).
130
+
131
+ > ⚠️ **Field name/polarity — `allowUnconstrainedRecipient`, NOT `requireRecipientIsAccount`.**
132
+ > The field is an **opt-out**, default-safe: `false` (the default, i.e. an omitted/zero-value
133
+ > bool) means the consuming call's output recipient is decoded and **must equal the account** —
134
+ > the safe posture. `true` is a deliberate opt-out that leaves the recipient **unconstrained**.
135
+ > Setting the bool `true` thinking it "requires/pins the recipient" gets the **opposite**
136
+ > behaviour on-chain. Either way, the recipient (and the consumed asset) can only be decoded for
137
+ > selectors in the fixed-offset set below — any other selector is denied under the default pin:
138
+ > `swapExactTokensForTokens`, V3 `exactInputSingle` (both SwapRouter layouts), Aave V2/V3
139
+ > `deposit`/`supply`, ERC-4626 `deposit`/`mint`.
@@ -0,0 +1,139 @@
1
+ # Reuse-first flow — register + configure a shared template
2
+
3
+ The whole point of a **shared** template: the logic is already deployed, so you skip the
4
+ deploy entirely. You give one SMA its own private config inside the singleton's
5
+ `mapping(address => …)` and register it on the kernel.
6
+
7
+ > **Two steps, not one (today).** The on-chain **intended** design is a single signed call —
8
+ > `MandateFactory.attach(account, template, params, configureDeadline, configureSig, kernelSig)`
9
+ > — that registers the template on the kernel AND writes its per-account config together.
10
+ > **The shipped `sailor` CLI does not implement that combined call yet.** `sailor mandate
11
+ > attach` performs **only the kernel registration** (`RegisterPermission` / the batch
12
+ > `registerPermissions`); it never calls `configure`/`configureDirect`, carries no `--params`,
13
+ > and builds no `Configure` EIP-712 signature. The SDK's `client.mandate.attach(...)` is a
14
+ > `notImplemented()` stub.
15
+ >
16
+ > A registered-but-unconfigured singleton has `isConfigured == false`, so the kernel's
17
+ > `evaluate()` **denies every call** — a registered-but-dead permission. You MUST configure
18
+ > separately, with `sailor mandate configure` (step 4 below). The flow below describes what
19
+ > actually works now.
20
+
21
+ ## On-chain mechanics (`MandateFactory` + `ConfigurablePermission`)
22
+
23
+ These are the **on-chain** entry points — the contract truth for what the combined/intended
24
+ flow does. The shipped CLI uses a subset of them today (registration only); the configure
25
+ half is driven directly for now.
26
+
27
+ Registration (kernel side — what `sailor mandate attach` does today):
28
+
29
+ | Function | Use |
30
+ |---|---|
31
+ | kernel `registerPermission(account, permission, deadline, sig)` | Register one template address on an SMA |
32
+ | kernel `registerPermissions(account, permissions[], deadline, sig)` | Register several in one signature (the `--address a,b,c` batch path) |
33
+ | kernel `revokePermissions(account, permissions[], deadline, sig)` | Remove (`sailor mandate revoke`) |
34
+
35
+ Configuration (singleton side — the half the CLI does NOT do today):
36
+
37
+ | Function | Use |
38
+ |---|---|
39
+ | `configure(account, params, deadline, sig)` | Set per-account config, gated by an EIP-712 `Configure(account, paramsHash, nonce, deadline, epoch)` signature from the account's `permissionSigner` (ECDSA or ERC-1271). |
40
+ | `configureDirect(account, params)` | Set per-account config, gated by `msg.sender == permissionSigner`. The working path when the owner IS the permissionSigner — a plain owner tx, no signature. |
41
+ | `reconfigure(...)` | Intended batch re-config path (`MandateFactory.reconfigure`); functionally `configure` again with a new blob. |
42
+
43
+ The intended combined call (`MandateFactory.attach`) and its batch/replace/detach siblings are
44
+ defined on the factory contract; they are the target state for a future one-step CLI command,
45
+ not something the current CLI drives.
46
+
47
+ The kernel evaluates a registered permission via `staticcall` with a per-permission gas cap;
48
+ a revert or over-gas is treated as `false` (fail-closed), never a kernel revert.
49
+
50
+ ## The steps (as they work today)
51
+
52
+ ### 1. Discover the address
53
+ ```bash
54
+ node SKILLS/sail-templates/catalog.mjs --chain <id>
55
+ ```
56
+ The address comes from [`deployed.json`](../deployed.json). **If the template isn't deployed on
57
+ your chain yet** (e.g. `SwapPermissionNoOracle` is not deployed anywhere), that's the
58
+ prerequisite: deploy the singleton once (Protocol team / `DeploySharedTemplates`) and record
59
+ its address there. A template address is only meaningful paired with that chain's kernel.
60
+
61
+ ### 2. Build the config blob
62
+ Encode the config tuple **exactly as the contract's `_applyConfig` decodes it** — the
63
+ authoritative tuples are in [config-schemas.md](config-schemas.md). Use `abi.encode(...)` in
64
+ that order, or the typed builder under `@sail/sdk/templates` **only after** verifying its param
65
+ tuple equals the source tuple (the SDK builders track a previously-deployed set and may differ
66
+ from these source contracts):
67
+ ```ts
68
+ import { boundedSwapTemplate } from "@sail/sdk/templates"; // verify params vs config-schemas.md first
69
+
70
+ const params = { routers, tokensIn, tokensOut, maxAmountPerTx, maxSlippageBps,
71
+ priceOracle, maxPriceAgeSec };
72
+ const blob = boundedSwapTemplate.encoder.encode(params);
73
+
74
+ // Always surface the human-readable summary + warnings to the user first:
75
+ const explanation = boundedSwapTemplate.explainer.explain(params);
76
+ ```
77
+
78
+ > ⚠️ **Encoding gotcha (fails closed).** Templates do NOT all encode the same way.
79
+ > `TransferPermission` / `WithdrawPermission` / `DepositPermission` decode **flat top-level
80
+ > params** (`abi.encode(address[], address[], uint256)`). `ApproveAndCallBatchPermission`
81
+ > decodes a **single wrapped struct** (`abi.encode(Config{…})`) — its blob starts with a `0x20`
82
+ > tuple-offset word. Wrapping a flat-params blob (or vice-versa) reverts at configure. **Always
83
+ > round-trip-decode the blob and `cast call`-simulate `configureDirect` before sending.**
84
+
85
+ ### 3. Register the singleton on the kernel (does NOT configure)
86
+ ```bash
87
+ sailor mandate attach --address <SHARED_ADDRESS> --sma <SMA> --label "<primitive>"
88
+ ```
89
+ This builds and submits the kernel `RegisterPermission` (or `registerPermissions` for a
90
+ comma-separated `--address` list). It does NOT call `configure`. After this step the address is
91
+ in `getPermissions(<SMA>)` but `isConfigured(<SMA>) == false`, so every dispatch is still
92
+ denied. Proceed to step 4.
93
+
94
+ ### 4. Configure the per-account bounds (the half that makes it live)
95
+ When the owner IS the SMA's `permissionSigner` (the default Sailor onboarding), drive
96
+ `configureDirect(account, <config blob>)` as a plain owner transaction — `msg.sender ==
97
+ permissionSigner` holds and no EIP-712 signature is needed — via the shipped command:
98
+ ```bash
99
+ sailor mandate configure --address <SHARED_ADDRESS> --sma <SMA> \
100
+ --template <TemplateName> --args-file ./config.json
101
+ # or with a pre-encoded blob:
102
+ sailor mandate configure --address <SHARED_ADDRESS> --sma <SMA> --params <0x-blob>
103
+ ```
104
+ It does, in order:
105
+ 1. **Pre-flight (no gas):** an `eth_call` simulation of `configureDirect` from `permissionSigner`. A revert here means the config is invalid before any gas is spent — fix the blob (see the encoding gotcha) and retry. Pass `--simulate-only` to stop here.
106
+ 2. **Send it:** pushes the `configureDirect` call to the owner wallet as an `arbitrary-tx` request through the signing station; the owner approves in the browser and the owner wallet sends the transaction.
107
+ 3. **Verify:** reads `isConfigured(<SMA>)` on the singleton and errors if it isn't `true`.
108
+
109
+ If the owner is **not** the `permissionSigner` (e.g. a separate mandate-signer / multisig), use
110
+ `configure(account, params, deadline, sig)` instead: construct the EIP-712 `Configure` digest
111
+ (bound to the kernel's current `registrationEpoch`), have the `permissionSigner` sign it, and
112
+ submit. `sailor mandate configure` does not build this signature for you — that path is still
113
+ manual.
114
+
115
+ ### 5. Verify off-chain (no gas)
116
+ Prove it accepts what you want and rejects what you don't, before trusting it:
117
+ ```bash
118
+ sailor mandate simulate --address <SHARED_ADDRESS> --sma <SMA> \
119
+ --target <router> --calldata <hex> --expect pass
120
+ # or a batch file:
121
+ sailor mandate simulate --address <SHARED_ADDRESS> --sma <SMA> --calls ./probe.json
122
+ ```
123
+
124
+ ### 6. Reconfigure when bounds change
125
+ New cap or allowlist? Re-encode the blob and repeat step 4 (`sailor mandate configure --force`,
126
+ or the manual `configure` signature path) — the address stays registered, no re-attach. (On-chain
127
+ `MandateFactory.reconfigure` is the intended batch path for this.)
128
+
129
+ ## Gotchas
130
+
131
+ - **Unconfigured = deny.** A freshly-registered shared template with no `configure()` has
132
+ `isConfigured == false` and denies everything. Register AND configure — don't stop at
133
+ `sailor mandate attach`.
134
+ - **`sailor mandate attach` is register-only.** It does not configure. This is the single most
135
+ common trap; see steps 3 and 4.
136
+ - **Caps are in base units** of the relevant token (e.g. `25_000_000` = 25 USDC).
137
+ - **permissionSigner is the trust anchor** for config. In production use a multisig / timelock
138
+ — whoever holds it can widen allowlists and caps on every account they signed for.
139
+ - The shared singletons are **unaudited examples**. Simulate (Step 5) is not optional.
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: sail-token-resolve
3
+ description: Resolve the tokens a user names by symbol or address into on-chain metadata (address, decimals) AND a cross-chain, cross-DEX liquidity map — which Sail chain and which protocol (Uniswap V3/V4, Sushiswap, PancakeSwap, Aerodrome…) actually hold liquidity, how deep each pool is, and whether the leg is swap-ready. Use BEFORE building any swap/DCA/LP/lending mandate, or whenever a user says "a portfolio of X and Y" / "DCA into A, B, C" / "can I swap X here?". Pass every symbol at once. Resolves ANY symbol — curated registry → keyless GeckoTerminal DEX index → on-chain symbol()+decimals() — and recommends which chain to act on. Runs the bundled `scripts/resolve-token.mjs` (no dependencies, no gas, no API key).
4
+ ---
5
+
6
+ # sail-token-resolve — tokens → addresses + where the liquidity lives
7
+
8
+ Users name tokens by symbol ("WETH", "UNI", "HYPE", "MORPHO") far more often than by
9
+ address, and they think in *portfolios* ("a DCA of USDC, UNI and MORPHO"), not single
10
+ legs. This skill takes **one or many** symbols and, for each, returns: the verified
11
+ on-chain **address + decimals** per chain, a **liquidity venue map** (which chain, which
12
+ DEX/protocol, which pool, how deep), whether it is **swap-ready**, and a **recommendation**
13
+ when there's a discrepancy — liquidity split across chains, no pool on your chain but a deep
14
+ one elsewhere, or no pool on any Sail chain at all.
15
+
16
+ **"token exists" ≠ "token is swap-ready."** A token can have a valid contract with zero
17
+ routable liquidity. Swap-ready (on-chain confirmed) means Uniswap V3 QuoterV2 returns a
18
+ non-zero USDC→token quote — Sail's executable fast-path route.
19
+
20
+ ## When to load
21
+
22
+ - The user names one or more tokens and you need addresses/decimals for a mandate.
23
+ - A user describes a **portfolio / DCA / basket** — resolve every symbol in one call.
24
+ - Before `sail-swap-quote` or `sail-template-swap` — both consume this skill's output.
25
+ - Whenever the user asks "can I swap X here?", "where's the best liquidity for X?", or
26
+ "which chain should I use for this strategy?"
27
+
28
+ ## Run it
29
+
30
+ ```bash
31
+ # From the project root (reads .sail/.env.local for RPCs + chain):
32
+ node scripts/resolve-token.mjs WETH # single token, configured chain(s)
33
+ node scripts/resolve-token.mjs LINK --chain unichain # force one chain
34
+ node scripts/resolve-token.mjs 0x4200…0006 --chain base # address input
35
+ node scripts/resolve-token.mjs USDC UNI HYPE MORPHO # PORTFOLIO → rich JSON
36
+ node scripts/resolve-token.mjs UNI --all-chains --json # scan every Sail mainnet
37
+ ```
38
+
39
+ Flags: `--chain <ethereum|unichain|base|arbitrum>` forces one chain; `--rpc <url>` overrides
40
+ the endpoint; `--all-chains` maps every Sail mainnet (even ones without an RPC configured —
41
+ those use GeckoTerminal-only data) so you can recommend "put your SMA on chain Y"; `--json`
42
+ forces the rich per-token map for a single token.
43
+
44
+ ## Output shapes (pick the right consumer)
45
+
46
+ | Invocation | stdout shape |
47
+ |---|---|
48
+ | 1 symbol, single configured chain / `--chain` | **bare object** — `{address, decimals, feeTier, swapReady, quote, venues[], …}` (the shape `sail-swap-quote` / `sail-template-swap` expect) |
49
+ | 1 symbol, ≥2 configured chains | **array** of bare objects (one per chain) |
50
+ | ≥2 symbols | **portfolio**: `{ tokens: [tokenWrapper…], summary }` |
51
+ | 1 symbol + `--json`/`--all-chains` | **token wrapper** (see below) |
52
+
53
+ A **token wrapper** is:
54
+
55
+ ```jsonc
56
+ {
57
+ "query": "UNI",
58
+ "chains": { // keyed by chain name; each value is a bare per-chain object
59
+ "base": { "address": "0x…", "decimals": 18, "swapReady": true, "feeTier": 10000,
60
+ "venues": [ … ], "bestVenue": { … }, "onchainVerified": true, … },
61
+ "unichain": { … }
62
+ },
63
+ "chainsWithLiquidity": ["unichain","base","arbitrum"],
64
+ "onSailChain": true,
65
+ "crossChain": { "action": "route", "deepestChain": "base", "note": "…human guidance…" }
66
+ }
67
+ ```
68
+
69
+ A **venue** (inside `venues[]`, sorted deepest-first, capped at 8; `venuesTotal` is the full
70
+ count) is:
71
+
72
+ ```jsonc
73
+ { "protocol": "uniswap-v3", // uniswap-v3 | uniswap-v4 | uniswap-v2 | sushiswap | pancakeswap | aerodrome | other
74
+ "dexId": "uniswap-v3-base", // raw GeckoTerminal id
75
+ "pool": "0x…", "feeTier": 500, // basis points (500 = 0.05%); null if the pool has no fee in its name
76
+ "pairedSymbol": "USDC", "pairedToken": "0x…",
77
+ "liquidityUsd": 8645941, "volume24hUsd": 22333927,
78
+ "sailRoutable": true, // Sail's fast path can route it (see below)
79
+ "quoteVerified": true } // a live on-chain QuoterV2 quote confirmed THIS venue
80
+ ```
81
+
82
+ ## How it works
83
+
84
+ 1. **Symbol → address** (two layers): curated registry (instant, offline) → GeckoTerminal
85
+ search (keyless CoinGecko DEX index) for anything not curated. Candidates are ranked by
86
+ pool liquidity; the on-chain `symbol()` check is the final authority (a wrong DEX-side
87
+ match is rejected, not trusted).
88
+ 2. **On-chain verify** — `symbol()` + `decimals()` via eth_call on every chain that has an
89
+ RPC. This is the source of truth (`decimalsSource: "onchain"`). On an `--all-chains` scan
90
+ of a chain with no RPC, metadata falls back to GeckoTerminal (`decimalsSource:
91
+ "geckoterminal-unverified"`) — flag this to the user before wiring it into a mandate.
92
+ 3. **Liquidity venue map** — `GET /networks/{net}/tokens/{addr}/pools` returns every pool
93
+ GeckoTerminal indexes, across **all DEXes** (Uniswap V3/V4, Sushiswap, PancakeSwap,
94
+ Aerodrome, …), with the protocol, pool address, fee tier and USD depth. One call per token
95
+ per chain.
96
+ 4. **Swap-readiness (on-chain confirmed)** — quotes USDC→token across fee tiers 500/3000/10000
97
+ via Uniswap V3 QuoterV2 and marks the matching venue `quoteVerified: true`. This is the only
98
+ *executable* signal; everything else in `venues[]` is informational.
99
+
100
+ ## `sailRoutable` — what Sail's fast path can actually swap
101
+
102
+ Sail's `sail-template-swap` fast path routes through **Uniswap V3** (everywhere) and the
103
+ **Uniswap V4** Universal Router (on Unichain). Those venues are marked `sailRoutable: true`.
104
+ Sushiswap, PancakeSwap, Aerodrome (and Uniswap V2) are detected and surfaced so you can see
105
+ *where the liquidity really is*, but they're `sailRoutable: false` — Sail can't route them via
106
+ the fast path. If the only liquidity is on a non-routable DEX, the token needs a custom mandate
107
+ (`sail-mandates`) or should be held. (Pools with absurd fees — >10% — are spam and are never
108
+ marked routable.)
109
+
110
+ ## How to present results to the user
111
+
112
+ Read `crossChain.action` (per token) and the portfolio `summary`, then advise:
113
+
114
+ - **`route`** — swap-ready on a configured chain. If it's routable on **more than one**
115
+ configured chain, surface both with their depths and ask which to use (or pick by where the
116
+ rest of the basket lives). Hand the chosen chain's bare object to `sail-swap-quote`.
117
+ - **`suggest-sma`** — no routable pool on the configured chain(s), but a deep one on another
118
+ Sail chain. Tell the user and **recommend deploying an SMA on that chain** for this leg
119
+ (e.g. "MORPHO has no Uniswap pool on Base; the deep pool is Uniswap V3 on Unichain — consider
120
+ an SMA on Unichain"). Don't silently drop it.
121
+ - **`manual-address`** — liquidity exists but only on a DEX Sail can't fast-route (e.g. only on
122
+ Aerodrome). Offer a custom mandate via `sail-mandates`, or hold the leg.
123
+ - **`hold-skip`** — no pool on any scanned Sail chain (the token may live on a non-Sail chain,
124
+ e.g. a HyperEVM-only asset). Recommend holding/dropping it from the strategy.
125
+
126
+ Always show the resolved **address + decimals + the chain/protocol/depth** you're acting on, so
127
+ the user can sanity-check before anything is signed.
128
+
129
+ ## Worked example — "create a DCA strategy of USDC, UNI, HYPE and MORPHO"
130
+
131
+ ```bash
132
+ node scripts/resolve-token.mjs USDC UNI HYPE MORPHO --all-chains
133
+ ```
134
+
135
+ A typical read of the result:
136
+ - **USDC** — the quote asset; swap-ready everywhere (`feeTier: null`, it *is* USDC).
137
+ - **UNI** — `route`; routable on Unichain/Base/Arbitrum. Pick by depth or by where the basket
138
+ concentrates.
139
+ - **HYPE** — `route` only on **Unichain** (a real ~$4M Uniswap V3 USDC pool); absent/illiquid
140
+ elsewhere. Tell the user HYPE is a Unichain-only leg here.
141
+ - **MORPHO** — `route` on **Base** (Uniswap V3 USDC pool); on other chains it exists but has no
142
+ routable USDC pool. If the project is configured for Base, good; if not, `suggest-sma`.
143
+
144
+ Then, for each `route`/chosen-chain leg, hand `(address, decimals, feeTier)` to
145
+ [`sail-swap-quote`](../sail-swap-quote/SKILL.md) → [`sail-template-swap`](../sail-template-swap/SKILL.md).
146
+
147
+ ## Important
148
+
149
+ - **Decimals are critical** — 25 USDC = `25_000_000` (6 dec); 1 WETH =
150
+ `1_000_000_000_000_000_000` (18 dec). Every cap in a mandate is base units. Trust
151
+ `decimalsSource: "onchain"`; treat `geckoterminal-unverified` as provisional.
152
+ - **Addresses are per-chain** — WETH on Unichain ≠ WETH on Base ≠ WETH on Arbitrum. Resolve and
153
+ verify separately per chain; never copy an address across chains.
154
+ - **`getPool` is unreliable** on some forks — swap-readiness trusts a non-zero QuoterV2 quote,
155
+ never the V3 factory's `getPool`.
156
+ - **Symbol ambiguity / scam collisions.** When a symbol isn't curated, the deepest-pool
157
+ candidate whose on-chain `symbol()` matches is used. A scam token can share a real symbol; the
158
+ liquidity ranking favours the canonical one, but for an obscure symbol glance at the resolved
159
+ `address` and `source` before wiring it in — or pass the intended contract's `0x` address
160
+ directly (address-input bypasses the DEX lookup). Also sanity-check a venue's `volume24hUsd`
161
+ against its `liquidityUsd`: a huge pool with near-zero volume is inflated/non-trading, and a
162
+ big pool whose `pairedSymbol` is a look-alike (e.g. a "HYPE/BASEDHYPE" pool) is a *different*
163
+ asset — depth there does not make *your* token swap-ready.
164
+ - **`bestVenue` is USDC-relevant, not the biggest pool.** It's the deepest *Sail-routable,
165
+ USDC-paired* pool (the one a USDC DCA would route through), so it can be smaller than the
166
+ largest pool in `venues[]` (which may be a WETH or look-alike pair).
167
+ - **Missing venues ≠ no liquidity.** A chain can be `swapReady: true` (from the on-chain
168
+ QuoterV2 probe) yet show empty `venues[]` with `venuesError` set if GeckoTerminal rate-limited
169
+ that call. Trust `swapReady`; to repopulate the venue map for one chain, re-run
170
+ `node scripts/resolve-token.mjs <SYM> --chain <name> --json`.
171
+ - **Rate limits** — GeckoTerminal is keyless and rate-limited; the script throttles and caches
172
+ calls. A big `--all-chains` portfolio takes a moment. Set `GECKO_MIN_SPACING_MS` to tune the
173
+ spacing. A transient GeckoTerminal failure on one chain sets `venuesError` and leaves
174
+ `swapReady` intact (it's from the on-chain probe) — it never aborts the whole run.
@@ -18,3 +18,12 @@ CHAIN_ID=8453
18
18
 
19
19
  # Optional: non-interactive passphrase (CI, GitHub Actions, launchd, systemd)
20
20
  # SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
21
+
22
+ # Advanced — exposing the dashboard/API beyond localhost.
23
+ # The UI server binds 127.0.0.1 (local only) by default. Its key-management
24
+ # endpoints are UNAUTHENTICATED, so only expose them behind your own auth
25
+ # (a reverse proxy with basic-auth, or a private tailnet — never the open internet).
26
+ # SAILOR_HOST=0.0.0.0 # bind all interfaces (LAN / behind a domain)
27
+ # SAILOR_TRUST_PROXY=1 # hop count (or IP allowlist) when behind a reverse proxy, so the rate limit keys on the real client IP (else it's a single global bucket)
28
+ # SAILOR_CORS_ORIGINS=https://sail.example.ts.net # extra allowed browser origins (comma-separated)
29
+ # SAILOR_RATE_LIMIT_PER_MIN=100 # throttle /api/signer + /api/onboard/generate-key (default 100; or set "rateLimitPerMin" in .sail/config.json)
@@ -1,26 +1,120 @@
1
- This is **your** Sailor project — for defining and running your agent's strategy. It is yours to edit; `sailor update` will not overwrite it.
1
+ This guide is for agents operating a scaffolded Sailor project. (Contributors to the Sailor codebase: see AGENTS.md at the monorepo root.)
2
2
 
3
- **How to operate Sailor** set up the environment, onboard, build mandates, run the agent lives in the skills below, not in this file. Start with `sail-overview`. The operating model, the onboarding flow, and the safety rules are there, so this file stays about *your* strategy.
3
+ > **This is a standalone project, not a clone of the Sailor repo.** `sailor init` scaffolds an independent directory with its own (or no) git history it does not share history with `github.com/sail-money/Sailor`. Do **not** add that repo as a remote or `git pull origin/main` from it: you'll hit "refusing to merge unrelated histories" and add/add conflicts on `AGENTS.md`, `README.md`, `package.json`, `.gitignore`, and `docs/`. To update the tooling, bump the `@sail.money/sailor` dependency (`npm i @sail.money/sailor@latest`), not by pulling the source repo.
4
4
 
5
- > Standalone project — not a clone of the Sailor repo. To update the tooling, bump the dependency (`npm i @sail.money/sailor@latest`); do not add that repo as a git remote.
5
+ ---
6
+
7
+ Sail Protocol is infrastructure for onchain Separately Managed Accounts run by AI agents. You create an SMA, keep full custody, and define exactly what your agent can do — cryptographically bound permissions you approve and can always revoke. The agent executes within those bounds on every transaction. It cannot exceed them.
8
+
9
+ I'm Sailor, the operator that sets this up. I'll help you create your SMA, build the permissions that bound your agent, and get your strategy running.
10
+
11
+ Here's where we're headed:
12
+
13
+ 1. Deploy your SMA and create your agent wallet
14
+ 2. Define your strategy
15
+ 3. Build, test, and sign your mandate
16
+ 4. Run your agent — locally or on a schedule
17
+ 5. Extend with notifications and a custom dashboard
18
+
19
+ Ready? Say **start** and I'll open the setup interface in your browser.
20
+
21
+ ---
22
+
23
+ # Instructions for the assistant
24
+
25
+ Everything below is for you, the assistant. The user sees the welcome above; you follow the flow below.
26
+
27
+ ## Your job in mandate design
28
+
29
+ When designing a mandate, your job is to help the operator express the **tightest, most complete mandate that captures their strategy's intent** — not the smallest one that compiles. Enumerate every constraint the strategy implies *and* every one the protocol can express for the venues involved; explain what each protects against. Separate them into **safety bounds** (caps, allowlists, slippage floors — loss/theft surfaces, enforced on-chain by default) and **strategy parameters** (cadence, schedule, rebalance timing — how the strategy runs, not a safety surface, so they live in agent logic). A stated strategy parameter is still required: wire it as an agent-side guard and confirm it before go-live — don't try to force it on-chain. A minimal mandate that merely compiles is a failure mode; the goal is the tightest mandate that expresses the strategy.
30
+
31
+ ## Voice
32
+
33
+ You are Sailor. Serious, precise, confident. No hype, no emojis, no exclamation marks. Explain *why*, not just *what* — the user is moving real funds. Use user-facing terms (SMA, mandate, permissions, agent wallet, owner). Assume crypto-native; teach the Sail-specific model.
34
+
35
+ Never overstate safety: custody is protected, but a mandate is only as correct as its permission contracts.
36
+
37
+ ## Authorization rule
38
+
39
+ During **setup**, always ask before anything that costs gas. Once the **mandate is signed and the agent is running**, the mandate is the authorization — the agent transacts autonomously. Do not ask per-dispatch.
40
+
41
+ ## First contact
42
+
43
+ When the user says start (or any first message), present the welcome above in full — definition, stage list, handoff line — before doing anything else. Do not launch the UI yet. After the user says start a second time (or confirms they are ready), THEN run `sailor ui start` and `sailor station start`. The welcome, the UI launch and the signing station launch are three separate beats separated by the user's go-ahead.
44
+
45
+ If the user's first message is an npm install command, run it, then present the welcome immediately after it completes — do not wait for another message.
46
+
47
+ ## Stage flow — track to completion
48
+
49
+ The five stages above are a checklist you drive to completion, not a list you mention once. Track which stages are done (read `.sail/` to infer progress) and lead the operator to the next incomplete stage. The flow is not finished when the agent goes live — it is finished when stage 5 has been offered.
50
+
51
+ - [ ] 1. SMA + agent wallet deployed
52
+ - [ ] 2. Strategy defined
53
+ - [ ] 3. Mandate built, tested, signed
54
+ - [ ] 4. Agent running (locally or scheduled)
55
+ - [ ] 5. Extend — notifications and a custom dashboard
56
+
57
+ After the agent is live (stage 4), **offer stage 5 by default**: one line on run/transaction notifications and one line on a strategy-specific dashboard, then ask if the operator wants either. Skipping stage 5 requires an explicit operator opt-out — never drop it silently. Hand off to **sail-extend** to build whatever they accept.
58
+
59
+ ## Project state — read `.sail/`, never ask
60
+
61
+ Determine the user's progress by reading `.sail/` — do not ask; read it.
62
+
63
+ | File | What it tells you |
64
+ |---|---|
65
+ | `config.json` | Project manifest: name, `chainId` (null until the user picks a chain), contract addresses |
66
+ | `account.json` | Active SMA: safe, owner, manager (agent wallet), chainId, saltNonce, deployedChains |
67
+ | `mandate.json` | The signed mandate the runner executes against (absent = not signed yet) |
68
+ | `keys/` | Encrypted geth-v3 keystores (agent wallet, mandate signer) — never read or print contents |
69
+ | `state/mandates.json` | Append-only record of every permission deployed/attached from this project |
70
+ | `runtime/` | Live process state: `ui.json` (dashboard pid/port), `server.json` (signing station url/pid) |
71
+ | `activity.jsonl` | Unified activity log — agent dispatches and owner signing decisions, one JSON per line |
72
+ | `.env.local` | RPC_URL / CHAIN_ID / per-chain RPC vars / SAIL_PASSPHRASE — never commit or print |
6
73
 
7
74
  ## Skills
8
75
 
9
- Detailed procedures live in skills under `.agents/skills/`. If your tooling does not auto-discover them, open the files directly — they are plain markdown.
76
+ Detailed procedures live in skills. If your tooling does not auto-discover skills, open these files directly — they are plain markdown.
10
77
 
11
78
  | Skill | Load when | Path |
12
79
  |---|---|---|
13
- | sail-setup | First-time environment readiness dependencies, install mode, then `sailor init` | `.agents/skills/sail-setup/SKILL.md` |
14
- | sail-overview | How Sailor works, which skill to use when, and the safety invariants — read this first | `.agents/skills/sail-overview/SKILL.md` |
15
- | sail-sailor | How to invoke sailor — local vs Docker, installMode detection, all CLI commands, `sailor update` | `.agents/skills/sail-sailor/SKILL.md` |
16
- | sail-onboarding | New project setup, resuming a partially set-up project, or running a CLI command | `.agents/skills/sail-onboarding/SKILL.md` |
80
+ | sail-onboarding | New project setup, or resuming a partially set-up project, documentation of sailor commands | `.agents/skills/sail-onboarding/SKILL.md` |
17
81
  | sail-project-info | Any question about project, account, mandate, chain, or environment state | `.agents/skills/sail-project-info/SKILL.md` |
18
82
  | sail-servers | Starting, stopping, or health-checking the dashboard or signing station | `.agents/skills/sail-servers/SKILL.md` |
19
- | sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts | `.agents/skills/sail-mandates/SKILL.md` |
20
- | sail-transactions | Building dispatches, running the agent, or debugging a transaction | `.agents/skills/sail-transactions/SKILL.md` |
21
- | sail-automation | Running the agent unattendedGitHub Actions, self-hosted runner, Docker, or daemon | `.agents/skills/sail-automation/SKILL.md` |
83
+ | sail-token-resolve | Resolving a token symbol/address to its on-chain address + decimals, and checking whether a Uniswap V3 pool exists (swap-readiness) | `.agents/skills/sail-token-resolve/SKILL.md` |
84
+ | sail-swap-quote | Fetching a live Uniswap V3 quote and the slippage-adjusted amountOutMinimum floor | `.agents/skills/sail-swap-quote/SKILL.md` |
85
+ | sail-templates | Registry + reuse guide for Sail's shared permission singletons which primitives exist as templates, per-chain deployment status (`deployed.json`), and the register→configure reuse flow. Start here before any template mandate | `.agents/skills/sail-templates/SKILL.md` |
86
+ | sail-template-swap | Bounded DEX swap / DCA mandate via the shared SwapPermission singleton (oracle-gated slippage band) — register + configure, no Solidity. The fast path for a swap/DCA strategy | `.agents/skills/sail-template-swap/SKILL.md` |
87
+ | sail-template-swap-no-oracle | Bounded swap for tokens with NO oracle via SwapPermissionNoOracle (single-pool hallucination guard, NOT manipulation-resistant) — reference-only, not yet deployed on any chain | `.agents/skills/sail-template-swap-no-oracle/SKILL.md` |
88
+ | sail-template-transfer | Bounded ERC-20 transfer to a mutable recipient allowlist via TransferPermission | `.agents/skills/sail-template-transfer/SKILL.md` |
89
+ | sail-template-withdraw | Bounded ERC-20 withdraw to ONE fixed recipient (owner-Safe consolidation) via WithdrawPermission | `.agents/skills/sail-template-withdraw/SKILL.md` |
90
+ | sail-template-deposit | Bounded deposit into ERC-4626 vaults / Aave v2–v3 via DepositPermission | `.agents/skills/sail-template-deposit/SKILL.md` |
91
+ | sail-template-borrow | Bounded lending borrow against Aave / Morpho / Compound with an on-chain LTV check via BorrowPermission | `.agents/skills/sail-template-borrow/SKILL.md` |
92
+ | sail-template-approve-batch | Atomic approve → call → reset (allowance bracket) in one batch via ApproveAndCallBatchPermission | `.agents/skills/sail-template-approve-batch/SKILL.md` |
93
+ | sail-transactions | Building dispatches or any EVM transaction for the agent | `.agents/skills/sail-transactions/SKILL.md` |
94
+ | sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts (the bespoke-Solidity escape hatch) | `.agents/skills/sail-mandates/SKILL.md` |
95
+ | sail-automation | Automating the agent — GitHub Actions, self-hosted runner, Docker, or local daemon | `.agents/skills/sail-automation/SKILL.md` |
22
96
  | sail-extend | Notifications or a custom dashboard, once the agent is live | `.agents/skills/sail-extend/SKILL.md` |
23
97
 
24
- ## Your strategy
98
+ **Shared singletons are the default for the common DeFi primitives.** A bounded swap, transfer, withdraw, deposit, borrow, or approve-and-call mandate should first try to **reuse** the matching pre-deployed singleton — register + configure, no Solidity, no per-SMA deploy. Start at `sail-templates` (the registry: which primitives exist, per-chain deployment status in `.agents/skills/sail-templates/deployed.json`, and the register→configure flow), then use the matching per-primitive skill: `sail-template-swap`, `sail-template-swap-no-oracle`, `sail-template-transfer`, `sail-template-withdraw`, `sail-template-deposit`, `sail-template-borrow`, or `sail-template-approve-batch`. Six of seven singletons are live (Base, Arbitrum, Unichain, Sepolia, Base Sepolia); `SwapPermissionNoOracle` is source-only so far, and Ethereum mainnet is not yet deployed. Reach for `sail-mandates` (author + deploy your own `IPermission`) only when the strategy needs a venue, contract, or bound the singletons cannot express.
99
+
100
+ **Bundled scripts** under `scripts/` are dependency-free tools that make the fast path quick and deterministic — run them from the project root so they read `.sail/.env.local`:
101
+ - `resolve-token.mjs <SYMBOL|ADDRESS>` → on-chain address + decimals + swap-ready fee tier (QuoterV2 probe across 500/3000/10000).
102
+ - `quote-swap.mjs --token-in … --token-out … --amount <baseUnits> --fee <tier>` → live quote + `amountOutMinimum`.
103
+ - `shared-template-addr.mjs <TemplateName>` → the singleton's deployed address on the active chain.
104
+
105
+ ## Invariants — apply to every turn
25
106
 
26
- Use the space below for notes about *your* strategy — what the agent should do, its bounds, and any project-specific context. This section is yours.
107
+ - Do not present the welcome and immediately launch the UI wait for the second "start"
108
+ - Do not describe, mention, or present any code in `src/` or `examples/` as the user's strategy — treat strategy definition as a blank slate; ask what they want
109
+ - Do not ask a running agent to confirm individual dispatches within its mandate
110
+ - Do not put an owner key in the terminal — owner signing is browser-only
111
+ - Do not hand-roll dispatch EIP-712 signatures — use `buildDispatchSignature` from the SDK
112
+ - Do not hardcode the dispatch model — detect it on-chain with `detectKernelCapabilities`
113
+ - Do not present example permissions as audited or as a supported menu
114
+ - Do not commit `SAIL_PASSPHRASE` or private keys
115
+ - ERC-20 `approve()` calls are NOT covered by supply, swap, or deposit permissions — every approve the strategy makes needs explicit coverage. Two non-mixable models: per-call (separate single dispatches, one `IPermission` each — the default) or atomic batch (one `IBatchPermission` authorizing the whole `[approve, action]` sequence). A normal `IPermission` cannot authorize a batch. Details: `.agents/skills/sail-mandates/references/approvals.md`
116
+ - Never authorize (attach) a permission before `forge test` and `sailor mandate simulate` both pass against samples derived from the user's strategy
117
+ - **Register ≠ configure for shared singletons.** `sailor mandate attach` only registers the address on the kernel (`isConfigured` stays `false`); the kernel denies every call until you also run `sailor mandate configure`. Stopping at `attach` is the most common trap. See `sail-templates` (reuse-flow) and `sail-template-swap`.
118
+ - **Resolve tokens before binding them.** Never guess a token address or assume a token that exists is swap-ready. Run `scripts/resolve-token.mjs` for symbol→address+decimals+swap-readiness; a decimals mismatch (USDC 6 vs most 18) silently mis-sizes every cap, and a token with no V3 pool will fail-closed on every dispatch. Addresses are per-chain — never copy one across chains.
119
+ - Do not pass `--args` inline JSON from PowerShell — use `--args-file` instead
120
+ - Operator intent and the strategy's stated bounds outrank any example. If the operator asks for a bound an example omits, include it. Never let an example's shape narrow a mandate below what the operator requested
@@ -89,5 +89,5 @@ Run `sailor doctor` (read-only, no gas, no keys):
89
89
  - on a conjunctive kernel, **probes each permission for pass-through** and flags any
90
90
  that would brick dispatch.
91
91
 
92
- See [AGENT_PLAYBOOK.md](../AGENT_PLAYBOOK.md) for the operational decision tree and the
93
- revert failure-mode catalog.
92
+ See [AGENTS.md](../AGENTS.md) (and the skills it routes to under `.agents/skills/`)
93
+ for the operational decision tree and the revert failure-mode catalog.
@@ -10,6 +10,10 @@ struct Context {
10
10
  uint256 value;
11
11
  uint256 blockTimestamp;
12
12
  uint256 blockNumber;
13
+ /// @dev Kernel registrationEpoch(account, permission) at dispatch. Configurable
14
+ /// permissions fail closed on a mismatch with the epoch stamped at configure()
15
+ /// time; permissions without post-deploy configuration can ignore it.
16
+ uint256 configEpoch;
13
17
  }
14
18
 
15
19
  interface IPermission {