@mysten-incubation/sui-agent-pass 0.1.7 → 0.1.9

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.
@@ -35,12 +35,14 @@
35
35
  "protocols": {
36
36
  "deepbook": {
37
37
  "packageId": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809",
38
- "publishedAt": "0x0e735f8c93a95722efd73521aca7a7652c0bb71ed1daf41b26dfd7d1ff71f748"
38
+ "publishedAt": "0x0e735f8c93a95722efd73521aca7a7652c0bb71ed1daf41b26dfd7d1ff71f748",
39
+ "indexerUrl": "https://deepbook-indexer.mainnet.mystenlabs.com"
39
40
  },
40
41
  "cetus": {
41
42
  "packageId": "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb",
42
43
  "publishedAt": "0x260693ec785a6e6c9d81d58c7d2ff72f1288ae0fa6a9725abe05a6478b11f084",
43
- "configId": "0xdaa46292632c3c4d8f31f23ea0f9b36a28ff3677e9684980e4438403a67a3d8f"
44
+ "configId": "0xdaa46292632c3c4d8f31f23ea0f9b36a28ff3677e9684980e4438403a67a3d8f",
45
+ "apiUrl": "https://api-sui.cetus.zone"
44
46
  },
45
47
  "navi": {
46
48
  "packageId": "0x66c91a8560cd64d73d93dd1ec7b61f3f21ad2f66553dd3d7038ca69255479bb7",
package/dist/chain.js CHANGED
@@ -661,6 +661,70 @@ export async function withMarketFacts(client, d, needs) {
661
661
  }
662
662
  return { ...d, coins, pools };
663
663
  }
664
+ /**
665
+ * Pools on DeepBook and Cetus that trade `coin` (against `other` when given), largest Cetus TVL
666
+ * first. The venues' public listings only nominate candidates; each one is then read from chain
667
+ * by `withMarketFacts`, which drops anything that is not a pool of those coins. Returns the
668
+ * deployment extended with the pools and coins found.
669
+ */
670
+ export async function findPools(client, d, coin, other) {
671
+ const want = normalizeStructTag(coin), with_ = other ? normalizeStructTag(other) : null;
672
+ const fits = (a, b) => {
673
+ const [x, y] = [normalizeStructTag(a), normalizeStructTag(b)];
674
+ return (x === want && (!with_ || y === with_)) || (y === want && (!with_ || x === with_));
675
+ };
676
+ const candidates = new Map();
677
+ for (const p of d.pools)
678
+ if (fits(p.coinA, p.coinB))
679
+ candidates.set(p.poolId, null);
680
+ const { deepbook, cetus } = d.protocols;
681
+ const listings = await Promise.all([
682
+ deepbook.indexerUrl
683
+ ? fetch(`${deepbook.indexerUrl}/get_pools`, { signal: AbortSignal.timeout(8_000) })
684
+ .then((r) => r.json())
685
+ .then((all) => all
686
+ .filter((p) => fits(p.base_asset_id, p.quote_asset_id))
687
+ .map((p) => [p.pool_id, null]))
688
+ .catch(() => [])
689
+ : [],
690
+ cetus.apiUrl
691
+ ? fetch(`${cetus.apiUrl}/v3/sui/clmm/stats_pools`, {
692
+ method: 'POST',
693
+ headers: { 'content-type': 'application/json' },
694
+ body: JSON.stringify({
695
+ filter: '',
696
+ sortBy: 'tvl',
697
+ sortOrder: 'desc',
698
+ limit: 20,
699
+ offset: 0,
700
+ coinTypes: [want],
701
+ }),
702
+ signal: AbortSignal.timeout(8_000),
703
+ })
704
+ .then((r) => r.json())
705
+ .then((r) => (r.data?.list ?? [])
706
+ .filter((p) => fits(p.coinA.coinType, p.coinB.coinType))
707
+ .map((p) => [p.pool, p.tvl ?? null]))
708
+ .catch(() => [])
709
+ : [],
710
+ ]);
711
+ for (const [id, tvl] of listings.flat())
712
+ candidates.set(normalizeSuiAddress(id), tvl);
713
+ const ranked = [...candidates]
714
+ .sort(([, a], [, b]) => Number(b ?? 0) - Number(a ?? 0))
715
+ .slice(0, 10);
716
+ let extended = d;
717
+ const found = [];
718
+ for (const [id, tvl] of ranked) {
719
+ const next = await withMarketFacts(client, extended, { coins: [], pools: [id] }).catch(() => null);
720
+ const pool = next?.pools.find((p) => p.poolId === normalizeSuiAddress(id));
721
+ if (!next || !pool || !fits(pool.coinA, pool.coinB))
722
+ continue;
723
+ extended = next;
724
+ found.push({ pool, tvlUsd: tvl === null ? null : Number(tvl).toFixed(0) });
725
+ }
726
+ return { d: extended, found };
727
+ }
664
728
  function structTagString(t) {
665
729
  const params = t.typeParams.map((p) => (typeof p === 'string' ? p : structTagString(p)));
666
730
  return `${t.address}::${t.module}::${t.name}${params.length ? `<${params.join(',')}>` : ''}`;
package/dist/domain.js CHANGED
@@ -164,10 +164,20 @@ export const DeploymentSchema = z
164
164
  packageId: address,
165
165
  /** The latest upgrade, for calls made from a PTB; the original is version-gated. */
166
166
  publishedAt: address,
167
+ /** DeepBook's public indexer, which lists every pool; used only to find candidates. */
168
+ indexerUrl: z.string().url().optional(),
167
169
  })
168
170
  .strict(),
169
171
  /** `packageId` defines the pool type; `publishedAt` is the upgrade that answers calls. */
170
- cetus: z.object({ packageId: address, publishedAt: address, configId: address }).strict(),
172
+ cetus: z
173
+ .object({
174
+ packageId: address,
175
+ publishedAt: address,
176
+ configId: address,
177
+ /** Cetus's public API, which lists pools by coin; used only to find candidates. */
178
+ apiUrl: z.string().url().optional(),
179
+ })
180
+ .strict(),
171
181
  navi: z
172
182
  .object({
173
183
  /** The package that defines `account::AccountCap`, which the pass holds. */
package/dist/mcp.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
7
  import { z } from 'zod';
8
8
  import { clientFor, deepbookSizes, readAllHoldings, readStatus } from './chain.js';
9
- import { coinInfo } from './domain.js';
9
+ import { coinInfo, DAY_MS } from './domain.js';
10
10
  export const result = (value) => ({
11
11
  content: [{ type: 'text', text: JSON.stringify(value) }],
12
12
  });
@@ -45,14 +45,51 @@ async function sdkInfo(directory) {
45
45
  note: published ? null : `${root} must be built (npm run build) before installing from it`,
46
46
  };
47
47
  }
48
- /** Which code answers: the package version and when it was built, so a stale server shows. */
49
- async function serverInfo() {
48
+ let latest = null;
49
+ /** The newest version on npm, asked at most once a day per process; null when npm is unreachable. */
50
+ async function latestVersion(name) {
51
+ if (latest && Date.now() - latest.at < DAY_MS)
52
+ return latest.version;
53
+ const version = await fetch(`https://registry.npmjs.org/${name.replace('/', '%2F')}/latest`, {
54
+ signal: AbortSignal.timeout(2_000),
55
+ })
56
+ .then((r) => (r.ok ? r.json() : null))
57
+ .then((j) => j?.version ?? null)
58
+ .catch(() => null);
59
+ latest = { at: Date.now(), version };
60
+ return version;
61
+ }
62
+ const newer = (a, b) => {
63
+ const [x, y] = [a, b].map((v) => v.split('-')[0].split('.').map(Number));
64
+ for (let i = 0; i < 3; i++)
65
+ if (x[i] !== y[i])
66
+ return (x[i] ?? 0) > (y[i] ?? 0);
67
+ return false;
68
+ };
69
+ /**
70
+ * Which code answers: the package version and when it was built, so a stale server shows, and
71
+ * for an installed package a newer release on npm with the command that takes it.
72
+ */
73
+ export async function serverInfo() {
50
74
  const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
51
75
  const built = await stat(fileURLToPath(import.meta.url)).then((s) => s.mtime.toISOString());
76
+ const installed = fileURLToPath(import.meta.url)
77
+ .split(sep)
78
+ .includes('node_modules');
79
+ const available = installed ? await latestVersion(pkg.name) : null;
52
80
  return {
53
81
  version: pkg.version,
54
82
  builtAt: built,
55
83
  note: 'This process keeps the code it started with. After this package is rebuilt or upgraded, restart the agent client so it starts a new server; compare builtAt.',
84
+ ...(available && newer(available, pkg.version)
85
+ ? {
86
+ update: {
87
+ latest: available,
88
+ command: `npm install -g ${pkg.name}@latest`,
89
+ then: 'Restart the agent client so it starts the new server; the agent-pass skill is linked to the package and follows it.',
90
+ },
91
+ }
92
+ : {}),
56
93
  };
57
94
  }
58
95
  /**
package/dist/plugin.js CHANGED
@@ -20,12 +20,12 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
20
20
  import { Transaction } from '@mysten/sui/transactions';
21
21
  import { fromBase64, normalizeSuiAddress } from '@mysten/sui/utils';
22
22
  import { z } from 'zod';
23
- import { clientFor, deepbookSizes, quoteSwap, readAllHoldings, readStatus, withMarketFacts, } from './chain.js';
23
+ import { clientFor, deepbookSizes, findPools, quoteSwap, readAllHoldings, readStatus, withMarketFacts, } from './chain.js';
24
24
  import { describePolicy, describeSetup, renderRows } from './describe.js';
25
25
  import { address, coinBySymbol, coinInfo, DAY_MS, defaultGasDaily, DeploymentSchema, display, isStable, limitUnits, poolsFor, recipientName, sameCoin, SetupSchema, showCoin, units, VenueSchema, } from './domain.js';
26
26
  import { passesByAgent } from './explorer.js';
27
27
  import { passEnded, returnGasFloat } from './float.js';
28
- import { failure, INSTRUCTIONS, registerDocs, result, sessionInfo, SIGN_ANNOTATIONS, SIGN_DESCRIPTION, } from './mcp.js';
28
+ import { failure, INSTRUCTIONS, registerDocs, result, serverInfo, sessionInfo, SIGN_ANNOTATIONS, SIGN_DESCRIPTION, } from './mcp.js';
29
29
  import { authenticated, body, ownerPageUrl, startServer } from './server.js';
30
30
  import { SessionSigner } from './signer.js';
31
31
  import { SessionStore, token } from './storage.js';
@@ -233,7 +233,7 @@ const rulesInput = {
233
233
  pools: z
234
234
  .array(address)
235
235
  .optional()
236
- .describe('pool object ids for a pair the deployment does not list (a DeepBook or Cetus pool holding exactly these two coins); verified on chain'),
236
+ .describe('pool ids from find_pools for a pair the network does not list; never ask the owner for one. Verified on chain'),
237
237
  perTrade: decimal.describe('sold coin per trade'),
238
238
  daily: decimal.describe('sold coin per UTC day'),
239
239
  limitPrice: decimal.describe("the owner's price limit. With a stablecoin on one side it is that stablecoin per one of the other coin: the most paid when buying, the least accepted when selling. Otherwise it is bought coin per one sold coin, the least accepted. Required, above 0, and only the owner sets it: ask them, showing the pool's current price from list_passes"),
@@ -311,7 +311,7 @@ async function toSetup(client, d0, input) {
311
311
  (!sw.pools || sw.pools.some((id) => normalizeSuiAddress(id) === p.poolId)));
312
312
  if (!pools.length)
313
313
  throw new Error(d.pools.length
314
- ? `No pool trades ${sw.sell} for ${sw.buy}${sw.venues ? ` on ${sw.venues.join(', ')}` : ''}${sw.pools ? ` among ${sw.pools.join(', ')}` : ''}; name a DeepBook or Cetus pool id for this pair in pools`
314
+ ? `No pool trades ${sw.sell} for ${sw.buy}${sw.venues ? ` on ${sw.venues.join(', ')}` : ''}${sw.pools ? ` among ${sw.pools.join(', ')}` : ''}; look one up with find_pools and pass its id in pools`
315
315
  : `Swaps are not offered on ${d.network}: it lists no pools. Propose a pass on a network that does, or leave swaps out`);
316
316
  return {
317
317
  coinIn,
@@ -405,6 +405,20 @@ export async function forgetSession(directory) {
405
405
  await registry.all();
406
406
  return registry.forget(basename(resolve(directory)));
407
407
  }
408
+ /**
409
+ * What a pool pays for ten units of its non-stable coin (coinA when neither or both are
410
+ * stablecoins), quoted like a rule's price; ten clears DeepBook's minimum order.
411
+ */
412
+ async function poolPrice(d, p) {
413
+ const flip = isStable(coinInfo(d, p.coinA).symbol) && !isStable(coinInfo(d, p.coinB).symbol);
414
+ const a = coinInfo(d, flip ? p.coinB : p.coinA), b = coinInfo(d, flip ? p.coinA : p.coinB);
415
+ const amountIn = (10n * 10n ** BigInt(a.decimals)).toString();
416
+ const q = await quoteSwap(clientFor(d), d, p, a.type, amountIn).catch(() => null);
417
+ if (!q || q.amountOut === '0')
418
+ return {};
419
+ const perUnit = (BigInt(q.amountOut) * 10n ** BigInt(a.decimals)) / BigInt(q.amountIn);
420
+ return { price: `${display(perUnit.toString(), b.decimals)} ${b.symbol} per ${a.symbol}` };
421
+ }
408
422
  export function createPluginMcp(registry) {
409
423
  const server = new McpServer({ name: 'agent-pass', version: '0.1.0' }, {
410
424
  instructions: `${INSTRUCTIONS} This server also runs the owner flow: propose_authorization turns reviewed rules into a pass proposal and opens the owner's wallet page; session_info (with wait) reports when the owner has signed and the on-chain policies match; revoke_pass reopens the owner page for revocation. Show the owner the exact "rules" text returned by propose_authorization before they sign; never paraphrase amounts.`,
@@ -420,30 +434,18 @@ export function createPluginMcp(registry) {
420
434
  annotations: { readOnlyHint: true },
421
435
  }, async ({ owner }) => {
422
436
  const path = registry.deploymentPath();
423
- // What the pool pays for ten units of its non-stable coin (coinA when neither or both are
424
- // stablecoins), quoted like a rule's price; ten clears DeepBook's minimum order.
425
- const price = async (d, p) => {
426
- const flip = isStable(coinInfo(d, p.coinA).symbol) && !isStable(coinInfo(d, p.coinB).symbol);
427
- const a = coinInfo(d, flip ? p.coinB : p.coinA), b = coinInfo(d, flip ? p.coinA : p.coinB);
428
- const amountIn = (10n * 10n ** BigInt(a.decimals)).toString();
429
- const q = await quoteSwap(clientFor(d), d, p, a.type, amountIn).catch(() => null);
430
- if (!q || q.amountOut === '0')
431
- return {};
432
- const perUnit = (BigInt(q.amountOut) * 10n ** BigInt(a.decimals)) / BigInt(q.amountIn);
433
- return { price: `${display(perUnit.toString(), b.decimals)} ${b.symbol} per ${a.symbol}` };
434
- };
435
437
  const markets = async (d) => ({
436
438
  network: d.network,
437
439
  coins: d.coins,
438
440
  pools: await Promise.all(d.pools.map(async (p) => ({
439
441
  venue: p.venue,
440
442
  pair: `${coinInfo(d, p.coinA).symbol}/${coinInfo(d, p.coinB).symbol}`,
441
- ...(await price(d, p)),
443
+ ...(await poolPrice(d, p)),
442
444
  }))),
443
445
  lending: Object.keys(d.protocols.navi.reserves).map((c) => coinInfo(d, c).symbol),
444
446
  ...(d.pools.length || Object.keys(d.protocols.navi.reserves).length
445
447
  ? {
446
- otherCoins: 'These are the coins and pools this package lists, not all it can use. Any other coin the owner holds goes in a transfer or swap rule by its full type; a swap of it also needs, in pools, the id of a DeepBook or Cetus pool holding exactly that pair. The server reads the coin and the pool from chain. Lending stays with the listed coins.',
448
+ otherCoins: 'These are the coins and pools this package lists, not all it can use. Any other coin the owner holds goes in a transfer or swap rule by its full type; for a swap, find_pools looks up the DeepBook and Cetus pools for it, and their ids go in pools. Never ask the owner for a pool id. The server reads the coin and the pool from chain. Lending stays with the listed coins.',
447
449
  }
448
450
  : {
449
451
  note: 'Transfers and staking only: this network lists no pools and no NAVI reserves, so swap and lending rules are refused here.',
@@ -465,11 +467,44 @@ export function createPluginMcp(registry) {
465
467
  }
466
468
  : {}),
467
469
  deployment: { path, exists: own.length > 0 },
470
+ server: await serverInfo(),
468
471
  networks: [...own, ...bundled.filter((b) => !own.some((o) => o.network === b.network))],
469
472
  });
470
473
  });
474
+ server.registerTool('find_pools', {
475
+ description: "Find the DeepBook and Cetus pools that trade a coin, optionally against one other coin, for a swap rule. Each result is read from chain and carries its venue, id, pair, both coin types, Cetus TVL in USD when known and the pool's current price. Use it instead of asking the owner for a pool; prefer the deepest pool, and when the coin is ambiguous show the owner their balance of it and this price so they can confirm it is the right token.",
476
+ inputSchema: {
477
+ coin: symbol.describe('the coin, by symbol when list_passes lists it, else by full type'),
478
+ with: symbol.optional().describe('the other coin of the pair, same form; any when omitted'),
479
+ network: z
480
+ .enum(['mainnet', 'testnet', 'devnet', 'localnet'])
481
+ .optional()
482
+ .describe('defaults to the network a new pass would use'),
483
+ },
484
+ annotations: { readOnlyHint: true, openWorldHint: true },
485
+ }, async ({ coin, with: other, network }) => {
486
+ try {
487
+ const d0 = await registry.deployment(undefined, network);
488
+ const type = (s) => (/::/.test(s) ? s : coinBySymbol(d0, s).type);
489
+ const { d, found } = await findPools(clientFor(d0), d0, type(coin), other ? type(other) : undefined);
490
+ const priced = await Promise.all(found.map(async ({ pool, tvlUsd }) => ({
491
+ venue: pool.venue,
492
+ poolId: pool.poolId,
493
+ pair: `${coinInfo(d, pool.coinA).symbol}/${coinInfo(d, pool.coinB).symbol}`,
494
+ coinA: pool.coinA,
495
+ coinB: pool.coinB,
496
+ tvlUsd,
497
+ ...(await poolPrice(d, pool)),
498
+ })));
499
+ // A pool that cannot price ten units holds too little to trade against.
500
+ return result({ network: d.network, pools: priced.filter((p) => 'price' in p) });
501
+ }
502
+ catch (error) {
503
+ return failure(error, 'Pool lookup failed');
504
+ }
505
+ });
471
506
  server.registerTool('propose_authorization', {
472
- description: "Create a new pass proposal from rules in human units (coins by symbol when list_passes lists them for the network, else by full type, with pool ids for a pair the network list lacks; amounts as decimal strings) and open the owner's review page, where they connect Slush, check the same rules and sign one transaction. That transaction creates the pass, converts the owner's coins into their own address balance as far as the caps need, issues the allowances the agent will draw on, installs the policies and gives the new agent key its gas; the owner's coins never enter the pass. Returns the exact rules text to show the owner, the review URL and the session address. Nothing is on chain until the owner signs; poll session_info with wait afterwards.",
507
+ description: "Create a new pass proposal from rules in human units (coins by symbol when list_passes lists them for the network, else by full type, with pool ids from find_pools for a pair the network list lacks; amounts as decimal strings) and open the owner's review page, where they connect Slush, check the same rules and sign one transaction. That transaction creates the pass, converts the owner's coins into their own address balance as far as the caps need, issues the allowances the agent will draw on, installs the policies and gives the new agent key its gas; the owner's coins never enter the pass. Returns the exact rules text to show the owner, the review URL and the session address. Nothing is on chain until the owner signs; poll session_info with wait afterwards.",
473
508
  inputSchema: {
474
509
  name: z
475
510
  .string()
package/docs/PLAYBOOK.md CHANGED
@@ -54,14 +54,17 @@ pool), so list Cetus as well when trades will be small. A network may list no po
54
54
  reserves; `list_passes` says so, and swap or lending rules are refused there.
55
55
 
56
56
  **Coins and pools.** The coins and pools `list_passes` shows for a network are the ones this
57
- package lists, not the only ones it can use. Name a listed coin by symbol. Name any other coin
58
- the owner holds by its full type, and for a swap give the id of a DeepBook or Cetus pool holding
59
- exactly that pair in `pools`. To find the type and the balance, ask the owner for their address
60
- and call `list_passes` with `owner`: it adds every coin that address holds per network. For the
61
- pool, ask the owner or look the pair up on the venue's own site. The server and the review page
62
- read such a coin's symbol and decimals and such a pool's venue from chain, never from you, and
63
- the page shows an unlisted coin with its type so the owner can tell a look-alike from the real
64
- one. Lending is limited to the coins the network lists as NAVI reserves.
57
+ package lists, not the only ones it can use. Name a listed coin by symbol, and any other coin the
58
+ owner holds by its full type. To find its type and balance, ask the owner for their address and
59
+ call `list_passes` with `owner`: it adds every coin that address holds per network. For a swap,
60
+ find the pool with `find_pools`, which looks up DeepBook and Cetus, reads each pool from chain and
61
+ quotes its current price; pick the deepest one for the pair and pass its id in `pools`. Never ask
62
+ the owner for a pool id. When you are unsure you have the right token (several coins share a
63
+ symbol, or the owner named it loosely), show the owner their balance of it and its current price
64
+ and ask them to confirm. The server and the review page read a coin's symbol and decimals and a
65
+ pool's venue from chain, never from you, and the page shows an unlisted coin with its type so
66
+ the owner can tell a look-alike from the real one. Lending is limited to the coins the network
67
+ lists as NAVI reserves.
65
68
 
66
69
  **Lifetime caps.** `caps` gives, per coin, the total the key may ever withdraw, for exactly the
67
70
  coins the rules spend (staking spends SUI). A daily limit may not exceed its coin's cap. Size
@@ -89,9 +92,9 @@ ask for a yes, unless they already stated every rule explicitly.
89
92
 
90
93
  ## 2. Propose the pass and wait for the signature
91
94
 
92
- `propose_authorization`, `propose_change`, `list_passes`, `revoke_pass` and `forget_pass` live on
93
- the stdio server (`agent-pass mcp`), which keeps every session on the machine. A per-session HTTP
94
- signer has only `session_info` and `sign_transaction`.
95
+ `propose_authorization`, `propose_change`, `list_passes`, `find_pools`, `revoke_pass` and
96
+ `forget_pass` live on the stdio server (`agent-pass mcp`), which keeps every session on the
97
+ machine. A per-session HTTP signer has only `session_info` and `sign_transaction`.
95
98
 
96
99
  The pass goes on the network the owner names (`network`, one of `list_passes.networks`), else on
97
100
  the first network listed. A private publish of one's own takes a `deployment` path instead.
@@ -122,6 +125,10 @@ Start with `session_info`. It returns:
122
125
  - `directory`, `sdk` and `server`: the session directory, how to install and import the SDK,
123
126
  and this server's version (after an upgrade the client must restart the server).
124
127
 
128
+ **Updates.** When `server.update` is present in `list_passes` or `session_info`, a newer release
129
+ is out. Tell the owner once per conversation, with `server.update.command` and the reminder to
130
+ restart the agent afterwards, then carry on with the task; never run the command yourself.
131
+
125
132
  Act by writing a Node script against the SDK. Run `sdk.install` once in your working directory,
126
133
  then:
127
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/sui-agent-pass",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "AI builds transactions. A session signer signs. Your pass enforces the rules.",
6
6
  "repository": {
@@ -59,14 +59,17 @@ pool), so list Cetus as well when trades will be small. A network may list no po
59
59
  reserves; `list_passes` says so, and swap or lending rules are refused there.
60
60
 
61
61
  **Coins and pools.** The coins and pools `list_passes` shows for a network are the ones this
62
- package lists, not the only ones it can use. Name a listed coin by symbol. Name any other coin
63
- the owner holds by its full type, and for a swap give the id of a DeepBook or Cetus pool holding
64
- exactly that pair in `pools`. To find the type and the balance, ask the owner for their address
65
- and call `list_passes` with `owner`: it adds every coin that address holds per network. For the
66
- pool, ask the owner or look the pair up on the venue's own site. The server and the review page
67
- read such a coin's symbol and decimals and such a pool's venue from chain, never from you, and
68
- the page shows an unlisted coin with its type so the owner can tell a look-alike from the real
69
- one. Lending is limited to the coins the network lists as NAVI reserves.
62
+ package lists, not the only ones it can use. Name a listed coin by symbol, and any other coin the
63
+ owner holds by its full type. To find its type and balance, ask the owner for their address and
64
+ call `list_passes` with `owner`: it adds every coin that address holds per network. For a swap,
65
+ find the pool with `find_pools`, which looks up DeepBook and Cetus, reads each pool from chain and
66
+ quotes its current price; pick the deepest one for the pair and pass its id in `pools`. Never ask
67
+ the owner for a pool id. When you are unsure you have the right token (several coins share a
68
+ symbol, or the owner named it loosely), show the owner their balance of it and its current price
69
+ and ask them to confirm. The server and the review page read a coin's symbol and decimals and a
70
+ pool's venue from chain, never from you, and the page shows an unlisted coin with its type so
71
+ the owner can tell a look-alike from the real one. Lending is limited to the coins the network
72
+ lists as NAVI reserves.
70
73
 
71
74
  **Lifetime caps.** `caps` gives, per coin, the total the key may ever withdraw, for exactly the
72
75
  coins the rules spend (staking spends SUI). A daily limit may not exceed its coin's cap. Size
@@ -94,9 +97,9 @@ ask for a yes, unless they already stated every rule explicitly.
94
97
 
95
98
  ## 2. Propose the pass and wait for the signature
96
99
 
97
- `propose_authorization`, `propose_change`, `list_passes`, `revoke_pass` and `forget_pass` live on
98
- the stdio server (`agent-pass mcp`), which keeps every session on the machine. A per-session HTTP
99
- signer has only `session_info` and `sign_transaction`.
100
+ `propose_authorization`, `propose_change`, `list_passes`, `find_pools`, `revoke_pass` and
101
+ `forget_pass` live on the stdio server (`agent-pass mcp`), which keeps every session on the
102
+ machine. A per-session HTTP signer has only `session_info` and `sign_transaction`.
100
103
 
101
104
  The pass goes on the network the owner names (`network`, one of `list_passes.networks`), else on
102
105
  the first network listed. A private publish of one's own takes a `deployment` path instead.
@@ -127,6 +130,10 @@ Start with `session_info`. It returns:
127
130
  - `directory`, `sdk` and `server`: the session directory, how to install and import the SDK,
128
131
  and this server's version (after an upgrade the client must restart the server).
129
132
 
133
+ **Updates.** When `server.update` is present in `list_passes` or `session_info`, a newer release
134
+ is out. Tell the owner once per conversation, with `server.update.command` and the reminder to
135
+ restart the agent afterwards, then carry on with the task; never run the command yourself.
136
+
130
137
  Act by writing a Node script against the SDK. Run `sdk.install` once in your working directory,
131
138
  then:
132
139