@dev.sail.money/sailor 1.3.0-224 → 1.3.0-225

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 (133) hide show
  1. package/package.json +1 -1
  2. package/packages/cli/dist/index.cjs +694 -133
  3. package/packages/cli/dist/server.cjs +15884 -17950
  4. package/packages/sdk/dist/abis/ConfigurablePermission.d.ts +130 -0
  5. package/packages/sdk/dist/abis/ConfigurablePermission.d.ts.map +1 -0
  6. package/packages/sdk/dist/abis/ConfigurablePermission.js +79 -0
  7. package/packages/sdk/dist/abis/ConfigurablePermission.js.map +1 -0
  8. package/packages/sdk/dist/abis/index.d.ts +1 -0
  9. package/packages/sdk/dist/abis/index.d.ts.map +1 -1
  10. package/packages/sdk/dist/abis/index.js +1 -0
  11. package/packages/sdk/dist/abis/index.js.map +1 -1
  12. package/packages/sdk/dist/index.d.ts +1 -0
  13. package/packages/sdk/dist/index.d.ts.map +1 -1
  14. package/packages/sdk/dist/index.js +1 -0
  15. package/packages/sdk/dist/index.js.map +1 -1
  16. package/packages/sdk/dist/intelligence.d.ts +1 -1
  17. package/packages/sdk/dist/intelligence.js +1 -1
  18. package/packages/ui/dist/assets/{add-HBssdE_q.js → add-QIu9SGBU.js} +1 -1
  19. package/packages/ui/dist/assets/{all-wallets-kgnKzcam.js → all-wallets-2auxp9E5.js} +1 -1
  20. package/packages/ui/dist/assets/{app-store-C7gfCF4e.js → app-store-BVtVvk-F.js} +1 -1
  21. package/packages/ui/dist/assets/{apple-DmsD7XsU.js → apple-Dq7Ppvqv.js} +1 -1
  22. package/packages/ui/dist/assets/{arrow-bottom-B3CywkhY.js → arrow-bottom-DpT3R4Rr.js} +1 -1
  23. package/packages/ui/dist/assets/{arrow-bottom-circle-Cl21xaXp.js → arrow-bottom-circle-hkf9bWhW.js} +1 -1
  24. package/packages/ui/dist/assets/{arrow-left-Bp_pqRAk.js → arrow-left-3PhiE4SD.js} +1 -1
  25. package/packages/ui/dist/assets/{arrow-right-CRK93G5I.js → arrow-right-DUIXJHib.js} +1 -1
  26. package/packages/ui/dist/assets/{arrow-top-DyDJtPkJ.js → arrow-top-Cf--02la.js} +1 -1
  27. package/packages/ui/dist/assets/{bank-BthuR5Ky.js → bank-CkRSSgJy.js} +1 -1
  28. package/packages/ui/dist/assets/{basic-BSdElBI1.js → basic-D9puI79I.js} +1 -1
  29. package/packages/ui/dist/assets/{browser-BMfpXAJE.js → browser-BC8qo0Bh.js} +1 -1
  30. package/packages/ui/dist/assets/{card-BEvVqDzc.js → card-BYuGTxNc.js} +1 -1
  31. package/packages/ui/dist/assets/{ccip-BALQiE-q.js → ccip-BS_rjbk9.js} +1 -1
  32. package/packages/ui/dist/assets/{checkmark-BqYZHVCR.js → checkmark-C-MZeapS.js} +1 -1
  33. package/packages/ui/dist/assets/{checkmark-bold-DjNGbayE.js → checkmark-bold-Bs3hadsW.js} +1 -1
  34. package/packages/ui/dist/assets/{chevron-bottom-BEHbtNKD.js → chevron-bottom-C8mqtkD4.js} +1 -1
  35. package/packages/ui/dist/assets/{chevron-left-C__dvO-Z.js → chevron-left-Dxj-vVDJ.js} +1 -1
  36. package/packages/ui/dist/assets/{chevron-right-9abfT5a8.js → chevron-right-BoIrOTtL.js} +1 -1
  37. package/packages/ui/dist/assets/{chevron-top-B2VeyvGH.js → chevron-top-BlgziB_P.js} +1 -1
  38. package/packages/ui/dist/assets/{chrome-store-bPC0PXv4.js → chrome-store-sDKm_6es.js} +1 -1
  39. package/packages/ui/dist/assets/{clock-BolGF8wp.js → clock-BwfFymxk.js} +1 -1
  40. package/packages/ui/dist/assets/{close-Bo18lqFQ.js → close-BewD5_E0.js} +1 -1
  41. package/packages/ui/dist/assets/{coinPlaceholder-Bz-0VLbz.js → coinPlaceholder-DRPvf0NA.js} +1 -1
  42. package/packages/ui/dist/assets/{compass-CKKiFCLR.js → compass-BHPvd2dw.js} +1 -1
  43. package/packages/ui/dist/assets/{copy-CYIav_IY.js → copy-BMv4Y3KF.js} +1 -1
  44. package/packages/ui/dist/assets/{core-8m_0OoS0.js → core-BMNH6XFj.js} +3 -3
  45. package/packages/ui/dist/assets/cursor-ir0cjMUl.js +3 -0
  46. package/packages/ui/dist/assets/{cursor-transparent-DE9tExWo.js → cursor-transparent-DT8YBqHU.js} +1 -1
  47. package/packages/ui/dist/assets/{desktop-CWNluv_t.js → desktop-BeePUUqi.js} +1 -1
  48. package/packages/ui/dist/assets/{disconnect-Db6qWs6T.js → disconnect-C2qT29qY.js} +1 -1
  49. package/packages/ui/dist/assets/{discord-Cj6mVoBK.js → discord-I4ToELfU.js} +1 -1
  50. package/packages/ui/dist/assets/{etherscan-CCFLsPvo.js → etherscan-3psXC5Ef.js} +1 -1
  51. package/packages/ui/dist/assets/{events-BGQ3xBjc.js → events-Cdt6abbf.js} +1 -1
  52. package/packages/ui/dist/assets/{exclamation-triangle-DJWx46rU.js → exclamation-triangle-BCaWksGJ.js} +1 -1
  53. package/packages/ui/dist/assets/{extension-BtaF8Z9u.js → extension-DfYavvbY.js} +1 -1
  54. package/packages/ui/dist/assets/{external-link-5wYZl3Lh.js → external-link-B8uBskz7.js} +1 -1
  55. package/packages/ui/dist/assets/{facebook-BoGz1nMk.js → facebook-BSvrXy3w.js} +1 -1
  56. package/packages/ui/dist/assets/{fallback-B2d-ODZG.js → fallback-BSMYROva.js} +1 -1
  57. package/packages/ui/dist/assets/{farcaster-AsPTw3iB.js → farcaster-CcAuzEDi.js} +1 -1
  58. package/packages/ui/dist/assets/{filters-D1-1MQtc.js → filters-umCx-UFU.js} +1 -1
  59. package/packages/ui/dist/assets/{github-B1FrwkiF.js → github-CBNoBl67.js} +1 -1
  60. package/packages/ui/dist/assets/{google-DnrnTDFz.js → google-yyR1jNOC.js} +1 -1
  61. package/packages/ui/dist/assets/{help-circle-CI10dCJN.js → help-circle-C0WjKnIM.js} +1 -1
  62. package/packages/ui/dist/assets/{id-B627Euj0.js → id-BwIK6e_N.js} +1 -1
  63. package/packages/ui/dist/assets/{image-3O2KHUgY.js → image-D5oAo1Te.js} +1 -1
  64. package/packages/ui/dist/assets/{index-CgsvP_iE.js → index-B_Gc8xPp.js} +1 -1
  65. package/packages/ui/dist/assets/{index-DDe72Fsi.js → index-BaEe036r.js} +1 -1
  66. package/packages/ui/dist/assets/{index-DKCg39xU.js → index-BuqyrXYX.js} +1 -1
  67. package/packages/ui/dist/assets/index-CPrhuDfP.js +1782 -0
  68. package/packages/ui/dist/assets/{index-DhVzwxqf.js → index-DCWU1RuS.js} +1 -1
  69. package/packages/ui/dist/assets/{index-CeZ-3oZK.js → index-DMNSiu42.js} +3 -3
  70. package/packages/ui/dist/assets/{index.es-ClarUTak.js → index.es-DImsg59a.js} +4 -4
  71. package/packages/ui/dist/assets/{info-CMmDZSrf.js → info-CtcvKk8x.js} +1 -1
  72. package/packages/ui/dist/assets/{info-circle-ChsNDy1e.js → info-circle-BIm6XYyt.js} +1 -1
  73. package/packages/ui/dist/assets/{lightbulb-DDlT2eHQ.js → lightbulb-CMj21RIi.js} +1 -1
  74. package/packages/ui/dist/assets/{mail-DyTAk3JR.js → mail-C805tcaU.js} +1 -1
  75. package/packages/ui/dist/assets/{metamask-sdk-CoU-3z33.js → metamask-sdk-DDk4vkrl.js} +1 -1
  76. package/packages/ui/dist/assets/{mobile-B1ZTQh8S.js → mobile-BEii3NyZ.js} +1 -1
  77. package/packages/ui/dist/assets/{more-BNIrTkTn.js → more-RSnRt81X.js} +1 -1
  78. package/packages/ui/dist/assets/{network-placeholder-BRBk5JfG.js → network-placeholder-s9JaqzCQ.js} +1 -1
  79. package/packages/ui/dist/assets/{nftPlaceholder-xtnGz2yb.js → nftPlaceholder-Ccur2yVc.js} +1 -1
  80. package/packages/ui/dist/assets/{off-BfgRQBAE.js → off-DUtG4rlI.js} +1 -1
  81. package/packages/ui/dist/assets/{parseSignature-DPmCI80m.js → parseSignature-BbS8pxpB.js} +1 -1
  82. package/packages/ui/dist/assets/{play-store-BFjPDZvv.js → play-store-Bt2HvO3e.js} +1 -1
  83. package/packages/ui/dist/assets/{plus-C3dNKGgI.js → plus-BtEzQu0h.js} +1 -1
  84. package/packages/ui/dist/assets/{qr-code-BovNFnu8.js → qr-code-xbQZ-Nio.js} +1 -1
  85. package/packages/ui/dist/assets/{recycle-horizontal-DdcYEE6i.js → recycle-horizontal-d9pzE6vq.js} +1 -1
  86. package/packages/ui/dist/assets/{refresh-P6W_osB6.js → refresh-B3-2Sch0.js} +1 -1
  87. package/packages/ui/dist/assets/{reown-logo-DbesEx7U.js → reown-logo-DBkmQAX8.js} +1 -1
  88. package/packages/ui/dist/assets/{search-DMB73W4z.js → search-dgDA8Ou5.js} +1 -1
  89. package/packages/ui/dist/assets/{secp256k1-URVniZ2R.js → secp256k1-C4g4FCze.js} +1 -1
  90. package/packages/ui/dist/assets/{send-BUsSvitv.js → send-CEDXr4l7.js} +1 -1
  91. package/packages/ui/dist/assets/{swapHorizontal-CjaXc-eY.js → swapHorizontal-Co6_f8oH.js} +1 -1
  92. package/packages/ui/dist/assets/{swapHorizontalBold-DuNmGzT_.js → swapHorizontalBold-k4d22FNy.js} +1 -1
  93. package/packages/ui/dist/assets/{swapHorizontalMedium-CgZZIpJT.js → swapHorizontalMedium-Des6w_mz.js} +1 -1
  94. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BC1bVmtH.js → swapHorizontalRoundedBold-CVd4538j.js} +1 -1
  95. package/packages/ui/dist/assets/{swapVertical-PyYv49m9.js → swapVertical-Cn_eRfv8.js} +1 -1
  96. package/packages/ui/dist/assets/{telegram-DaCrUlL0.js → telegram-BE46RC0s.js} +1 -1
  97. package/packages/ui/dist/assets/{three-dots-D9tEmhOK.js → three-dots-CvqWsUWa.js} +1 -1
  98. package/packages/ui/dist/assets/{twitch-2m8_XY_5.js → twitch-9V7YXB0X.js} +1 -1
  99. package/packages/ui/dist/assets/{twitterIcon-BQ4fWtR1.js → twitterIcon-DbUxk6yk.js} +1 -1
  100. package/packages/ui/dist/assets/{verify-DJtFLNUq.js → verify-CQSnwVrh.js} +1 -1
  101. package/packages/ui/dist/assets/{verify-filled-B2cSEFoP.js → verify-filled-DG_hDTr8.js} +1 -1
  102. package/packages/ui/dist/assets/{w3m-modal-YBSDQDS1.js → w3m-modal-BsHGIrg7.js} +1 -1
  103. package/packages/ui/dist/assets/{wallet-C7Gv760P.js → wallet-lbVIReb_.js} +1 -1
  104. package/packages/ui/dist/assets/{wallet-placeholder-DtZ8_f4p.js → wallet-placeholder-C_ndmKAs.js} +1 -1
  105. package/packages/ui/dist/assets/{walletconnect-DsIIuQfT.js → walletconnect-ByDpG-E0.js} +1 -1
  106. package/packages/ui/dist/assets/{warning-circle-CTMOrGt2.js → warning-circle-hrS3II0d.js} +1 -1
  107. package/packages/ui/dist/assets/{x-BmL9CVvs.js → x-BPMn3x5J.js} +1 -1
  108. package/packages/ui/dist/index.html +1 -1
  109. package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
  110. package/templates/default/.agents/skills/sail-swap-quote/SKILL.md +76 -0
  111. package/templates/default/.agents/skills/sail-template-approve-batch/SKILL.md +98 -0
  112. package/templates/default/.agents/skills/sail-template-borrow/SKILL.md +68 -0
  113. package/templates/default/.agents/skills/sail-template-deposit/SKILL.md +61 -0
  114. package/templates/default/.agents/skills/sail-template-swap/SKILL.md +207 -0
  115. package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +100 -0
  116. package/templates/default/.agents/skills/sail-template-transfer/SKILL.md +52 -0
  117. package/templates/default/.agents/skills/sail-template-withdraw/SKILL.md +50 -0
  118. package/templates/default/.agents/skills/sail-templates/SKILL.md +142 -0
  119. package/templates/default/.agents/skills/sail-templates/catalog.mjs +200 -0
  120. package/templates/default/.agents/skills/sail-templates/deployed.json +133 -0
  121. package/templates/default/.agents/skills/sail-templates/oracles/IOracle.sol +13 -0
  122. package/templates/default/.agents/skills/sail-templates/oracles/UniV3TwapOracle.sol +114 -0
  123. package/templates/default/.agents/skills/sail-templates/oracles/libs/FullMath.sol +62 -0
  124. package/templates/default/.agents/skills/sail-templates/oracles/libs/TickMath.sol +44 -0
  125. package/templates/default/.agents/skills/sail-templates/references/config-schemas.md +139 -0
  126. package/templates/default/.agents/skills/sail-templates/references/reuse-flow.md +139 -0
  127. package/templates/default/.agents/skills/sail-token-resolve/SKILL.md +174 -0
  128. package/templates/default/AGENTS.md +21 -2
  129. package/templates/default/scripts/quote-swap.mjs +211 -0
  130. package/templates/default/scripts/resolve-token.mjs +992 -0
  131. package/templates/default/scripts/shared-template-addr.mjs +110 -0
  132. package/packages/ui/dist/assets/cursor-D10zW2HG.js +0 -3
  133. package/packages/ui/dist/assets/index-ohx1EwWJ.js +0 -1789
@@ -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.
@@ -40,7 +40,7 @@ During **setup**, always ask before anything that costs gas. Once the **mandate
40
40
 
41
41
  ## First contact
42
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`. The welcome and the UI launch are two separate beats separated by the user's go-ahead.
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
44
 
45
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
46
 
@@ -80,11 +80,28 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
80
80
  | sail-onboarding | New project setup, or resuming a partially set-up project, documentation of sailor commands | `.agents/skills/sail-onboarding/SKILL.md` |
81
81
  | sail-project-info | Any question about project, account, mandate, chain, or environment state | `.agents/skills/sail-project-info/SKILL.md` |
82
82
  | sail-servers | Starting, stopping, or health-checking the dashboard or signing station | `.agents/skills/sail-servers/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` |
83
93
  | sail-transactions | Building dispatches or any EVM transaction for the agent | `.agents/skills/sail-transactions/SKILL.md` |
84
- | sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts | `.agents/skills/sail-mandates/SKILL.md` |
94
+ | sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts (the bespoke-Solidity escape hatch) | `.agents/skills/sail-mandates/SKILL.md` |
85
95
  | sail-automation | Automating the agent — GitHub Actions, self-hosted runner, Docker, or local daemon | `.agents/skills/sail-automation/SKILL.md` |
86
96
  | sail-extend | Notifications or a custom dashboard, once the agent is live | `.agents/skills/sail-extend/SKILL.md` |
87
97
 
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
+
88
105
  ## Invariants — apply to every turn
89
106
 
90
107
  - Do not present the welcome and immediately launch the UI — wait for the second "start"
@@ -97,5 +114,7 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
97
114
  - Do not commit `SAIL_PASSPHRASE` or private keys
98
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`
99
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.
100
119
  - Do not pass `--args` inline JSON from PowerShell — use `--args-file` instead
101
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
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ // quote-swap.mjs — live Uniswap V3 quote via QuoterV2 + an amountOutMinimum floor.
3
+ //
4
+ // Takes resolved tokens + an exact amount (from resolve-token.mjs), returns the
5
+ // expected output and a slippage-adjusted amountOutMinimum ready to embed in a
6
+ // swap dispatch. Pure JS, no dependencies.
7
+ //
8
+ // node scripts/quote-swap.mjs \
9
+ // --token-in 0x078D...AD6 --decimals-in 6 \
10
+ // --token-out 0x4200...0006 --decimals-out 18 \
11
+ // --amount 25000000 --fee 3000 --slippage-bps 100
12
+ //
13
+ // Output: one JSON object on stdout; human notes on stderr.
14
+
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { resolve as resolvePath } from "node:path";
17
+
18
+ const CHAINS = {
19
+ ethereum: { chainId: 1, quoterV2: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" },
20
+ unichain: { chainId: 130, quoterV2: "0x385a5cf5f83e99f7bb2852b6a19c3538b9fa7658" },
21
+ base: { chainId: 8453, quoterV2: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a" },
22
+ arbitrum: { chainId: 42161, quoterV2: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" },
23
+ };
24
+
25
+ const SEL_QUOTE = "0xc6a5026a"; // quoteExactInputSingle((address,address,uint256,uint24,uint160))
26
+ const ADDR_ZERO = "0x" + "0".repeat(40);
27
+
28
+ // ── minimal ABI encode/decode (mirrors resolve-token.mjs) ───────────────────────
29
+ function pad32(h) {
30
+ h = h.toLowerCase().replace(/^0x/, "");
31
+ if (h.length < 64) h = "0".repeat(64 - h.length) + h;
32
+ return h;
33
+ }
34
+ function uintToHex(n) {
35
+ let h = n.toString(16);
36
+ if (h.length % 2) h = "0" + h;
37
+ return pad32(h);
38
+ }
39
+ function encodeQuoteCall(tokenIn, tokenOut, amountIn, fee) {
40
+ return SEL_QUOTE + pad32(tokenIn) + pad32(tokenOut) + uintToHex(amountIn) + uintToHex(BigInt(fee)) + uintToHex(0n);
41
+ }
42
+ function decodeUint(hex, word = 0) {
43
+ const h = hex.toLowerCase().replace(/^0x/, "");
44
+ const w = h.slice(word * 64, word * 64 + 64);
45
+ return w ? BigInt("0x" + w) : 0n;
46
+ }
47
+
48
+ async function ethCall(rpc, to, data, from = ADDR_ZERO) {
49
+ const res = await fetch(rpc, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to, data, from }, "latest"] }),
53
+ signal: AbortSignal.timeout(15_000),
54
+ });
55
+ if (!res.ok) throw new Error(`eth_call HTTP ${res.status}`);
56
+ const json = await res.json();
57
+ if (json.error) throw new Error(`QuoterV2 reverted: ${JSON.stringify(json.error)}`);
58
+ if (!json.result) throw new Error("eth_call returned no result");
59
+ return json.result;
60
+ }
61
+
62
+ function readSailEnv() {
63
+ const p = resolvePath(process.cwd(), ".sail", ".env.local");
64
+ const out = {};
65
+ if (existsSync(p)) {
66
+ for (const line of readFileSync(p, "utf8").split("\n")) {
67
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
68
+ if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ function readSailConfig() {
74
+ const p = resolvePath(process.cwd(), ".sail", "config.json");
75
+ if (!existsSync(p)) return {};
76
+ try {
77
+ return JSON.parse(readFileSync(p, "utf8"));
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+ function resolveChain(chainFlag) {
83
+ if (chainFlag) {
84
+ const c = CHAINS[chainFlag.toLowerCase()];
85
+ if (c) return { name: chainFlag.toLowerCase(), ...c };
86
+ for (const [name, cfg] of Object.entries(CHAINS)) if (String(cfg.chainId) === String(chainFlag)) return { name, ...cfg };
87
+ throw new Error(`Unknown chain "${chainFlag}". Known: ${Object.keys(CHAINS).join(", ")}`);
88
+ }
89
+ const env = readSailEnv();
90
+ const cfg = readSailConfig();
91
+ const id = env.CHAIN_ID ?? cfg.chainId;
92
+ if (id) for (const [name, c] of Object.entries(CHAINS)) if (String(c.chainId) === String(id)) return { name, ...c };
93
+ throw new Error(`Could not resolve chain. Pass --chain <${Object.keys(CHAINS).join("|")}>.`);
94
+ }
95
+ function resolveRpc(chain, rpcFlag) {
96
+ if (rpcFlag) return rpcFlag;
97
+ const env = readSailEnv();
98
+ // Mirror resolve-token.mjs / packages/cli getRpcUrl(): named var, then chainId-keyed
99
+ // var (RPC_URL_8453 — written by the UI / used by multi-chain projects), then generic.
100
+ const nameVar = `${chain.name.toUpperCase().replace("-", "_")}_RPC_URL`;
101
+ const idVar = `RPC_URL_${chain.chainId}`;
102
+ return env[nameVar] ?? env[idVar] ?? env.RPC_URL ?? null;
103
+ }
104
+
105
+ function parseArgs(argv) {
106
+ const out = {};
107
+ for (let i = 0; i < argv.length; i++) {
108
+ const a = argv[i];
109
+ if (a.startsWith("--")) {
110
+ const key = a.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
111
+ out[key] = argv[++i];
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+
117
+ function fmtUnits(valueBI, decimals) {
118
+ // bigint base units → human string, preserving precision
119
+ const neg = valueBI < 0n;
120
+ let s = (neg ? -valueBI : valueBI).toString().padStart(decimals + 1, "0");
121
+ const intPart = s.slice(0, s.length - decimals);
122
+ const fracPart = decimals > 0 ? s.slice(s.length - decimals) : "";
123
+ const trimmedFrac = fracPart.replace(/0+$/, "");
124
+ let out = trimmedFrac.length ? `${intPart}.${trimmedFrac}` : intPart;
125
+ if (decimals > 0 && !trimmedFrac) out = `${intPart}.0`;
126
+ return (neg ? "-" : "") + out;
127
+ }
128
+
129
+ function errMsg(e) {
130
+ return e && typeof e.message === "string" ? e.message : String(e);
131
+ }
132
+
133
+ async function main() {
134
+ const a = parseArgs(process.argv.slice(2));
135
+ if (process.argv.length <= 2 || a.help) {
136
+ process.stderr.write(
137
+ "Usage: quote-swap.mjs --token-in <addr> --token-out <addr> --amount <baseUnits>\n" +
138
+ " --decimals-in <n> --decimals-out <n> --fee <tier> [--slippage-bps <n>] [--chain <>] [--rpc URL]\n",
139
+ );
140
+ process.exit(process.argv.length <= 2 ? 1 : 0);
141
+ }
142
+
143
+ const need = ["tokenIn", "tokenOut", "amount", "decimalsIn", "decimalsOut", "fee"];
144
+ for (const k of need) if (a[k] === undefined) throw new Error(`Missing --${k.replace(/([A-Z])/g, "-$1").toLowerCase()}`);
145
+
146
+ const chain = resolveChain(a.chain);
147
+ const rpc = resolveRpc(chain, a.rpc);
148
+ if (!rpc) throw new Error(`No RPC for ${chain.name}. Pass --rpc or set RPC_URL in .sail/.env.local.`);
149
+
150
+ const tokenIn = a.tokenIn;
151
+ const tokenOut = a.tokenOut;
152
+ const amountIn = BigInt(a.amount);
153
+ const fee = Number(a.fee);
154
+ const decimalsIn = Number(a.decimalsIn);
155
+ const decimalsOut = Number(a.decimalsOut);
156
+ const slippageBps = a.slippageBps !== undefined ? Number(a.slippageBps) : 100; // default 1%
157
+ if (!Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps > 10_000) {
158
+ throw new Error(`--slippage-bps must be an integer in [0, 10000], got "${a.slippageBps}".`);
159
+ }
160
+
161
+ const data = encodeQuoteCall(tokenIn, tokenOut, amountIn, fee);
162
+ const ret = await ethCall(rpc, chain.quoterV2, data);
163
+ const amountOut = decodeUint(ret, 0); // word 0
164
+ const gasEstimate = decodeUint(ret, 3); // word 3
165
+
166
+ if (amountOut === 0n) {
167
+ throw new Error(
168
+ `QuoterV2 returned 0 for ${amountIn} (tokenIn) → tokenOut at fee ${fee}. The pool is empty or does not exist at this tier.`,
169
+ );
170
+ }
171
+
172
+ const amountOutMinimum = (amountOut * BigInt(10_000 - slippageBps)) / 10_000n;
173
+ // price = (amountOut / 10^decOut) / (amountIn / 10^decIn) → units of tokenOut per tokenIn
174
+ const humanIn = fmtUnits(amountIn, decimalsIn);
175
+ const humanOut = fmtUnits(amountOut, decimalsOut);
176
+ const humanMin = fmtUnits(amountOutMinimum, decimalsOut);
177
+
178
+ const out = {
179
+ tokenIn,
180
+ tokenOut,
181
+ decimalsIn,
182
+ decimalsOut,
183
+ amountIn: amountIn.toString(),
184
+ amountOut: amountOut.toString(),
185
+ amountOutMinimum: amountOutMinimum.toString(),
186
+ slippageBps,
187
+ fee,
188
+ chain: chain.name,
189
+ chainId: chain.chainId,
190
+ gasEstimate: gasEstimate.toString(),
191
+ human: {
192
+ amountIn: humanIn,
193
+ amountOut: humanOut,
194
+ amountOutMinimum: humanMin,
195
+ price: `${humanOut} out per ${humanIn} in`,
196
+ },
197
+ note: "amountOutMinimum is the slippage floor — embed it in the swap calldata the agent dispatches.",
198
+ };
199
+
200
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
201
+ process.stderr.write(
202
+ `\nQuote: ${humanIn} in → ${humanOut} out (fee ${fee})\n` +
203
+ ` amountOutMinimum (${slippageBps / 100}% slip): ${humanMin}\n` +
204
+ ` gas estimate: ${gasEstimate}\n`,
205
+ );
206
+ }
207
+
208
+ main().catch((err) => {
209
+ process.stderr.write(`\nquote-swap failed: ${errMsg(err)}\n`);
210
+ process.exit(1);
211
+ });