@mysten-incubation/sui-agent-pass 0.1.8 → 0.1.10

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/describe.js CHANGED
@@ -277,6 +277,8 @@ const normalizeType = (type) => type.replace(/^0x0*([0-9a-f]+)::/i, (_, a) => `$
277
277
  /** The owner's rule behind an abort, from the codes the policy modules define. */
278
278
  export function describeRefusal(d, error, recipient) {
279
279
  const abort = error.MoveAbort;
280
+ if (error.message === 'InsufficientBalanceForWithdraw')
281
+ return "The owner's address balance does not hold enough of this coin right now. This is not one of the owner's rules: the action can run once the balance covers it (InsufficientBalanceForWithdraw)";
280
282
  if (!abort?.location?.module)
281
283
  return error.message;
282
284
  const code = Number(abort.cleverError?.errorCode ?? abort.abortCode);
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. */
@@ -399,25 +409,45 @@ export function drawable(setup) {
399
409
  return Object.fromEntries(Object.entries(need).map(([c, v]) => [c, v.toString()]));
400
410
  }
401
411
  /**
402
- * Allowances draw only from the owner's address balance, never from coin objects. What the
403
- * authorization converts from the owner's coins into their address balance is, per coin, the
404
- * shortfall between what the rules can draw (`drawable`) and what that balance already holds.
405
- * Converted coins stay the owner's to spend.
412
+ * Allowances draw only from the owner's address balance, never from coin objects. The
413
+ * authorization converts coin objects into that balance, per coin, as far as the rules can draw
414
+ * (`drawable`) and the owner has them; the caps are limits, not money that must exist at signing.
415
+ * SUI keeps the stipend and the fee reserve in coin objects for the transaction itself. `mintable`
416
+ * is a demo coin the transaction mints instead, converted in full. Converted coins stay the
417
+ * owner's to spend.
418
+ */
419
+ export function conversion(setup, held, mintable = null) {
420
+ const of = (coin) => held[normalizeStructTag(coin)];
421
+ return Object.fromEntries(Object.entries(drawable(setup)).map(([coin, want]) => {
422
+ const short = BigInt(want) - BigInt(of(coin)?.addressBalance ?? '0');
423
+ if (short <= 0n)
424
+ return [coin, '0'];
425
+ if (mintable && sameCoin(mintable, coin))
426
+ return [coin, short.toString()];
427
+ let spare = BigInt(of(coin)?.coinBalance ?? '0');
428
+ if (sameCoin(coin, SUI))
429
+ spare -= BigInt(setup.gasAmount) + OWNER_FEE_RESERVE;
430
+ return [coin, (spare <= 0n ? 0n : short < spare ? short : spare).toString()];
431
+ }));
432
+ }
433
+ /**
434
+ * What the rules can draw beyond what the address balance will hold right after signing, per
435
+ * coin. Not an error: withdrawals work while the balance covers them, and swap proceeds land in
436
+ * it. Coins with nothing uncovered are left out.
406
437
  */
407
- export function conversion(setup, held) {
408
- const have = (coin) => BigInt(held[normalizeStructTag(coin)] ?? '0');
409
- return Object.fromEntries(Object.entries(drawable(setup)).map(([coin, want]) => [
410
- coin,
411
- (BigInt(want) > have(coin) ? BigInt(want) - have(coin) : 0n).toString(),
412
- ]));
438
+ export function uncovered(setup, held, mintable = null) {
439
+ const convert = conversion(setup, held, mintable);
440
+ return Object.fromEntries(Object.entries(drawable(setup)).flatMap(([coin, want]) => {
441
+ const covered = BigInt(held[normalizeStructTag(coin)]?.addressBalance ?? '0') + BigInt(convert[coin] ?? '0');
442
+ return covered >= BigInt(want) ? [] : [[coin, { drawable: want, covered: covered.toString() }]];
443
+ }));
413
444
  }
414
445
  /**
415
- * SUI the owner must hold in total to sign, address balance and coin objects together: what the
416
- * rules may draw, the stipend and a fee reserve. Gas comes from either side, so nothing has to
417
- * be a coin object; the authorization consolidates what it touches into the address balance.
446
+ * SUI the owner must hold to sign, address balance and coin objects together: only what this
447
+ * transaction moves, the agent's gas stipend, plus a fee reserve. The caps need nothing up front.
418
448
  */
419
449
  export function requiredSui(setup) {
420
- return (BigInt(drawable(setup)[SUI]) + BigInt(setup.gasAmount) + OWNER_FEE_RESERVE).toString();
450
+ return (BigInt(setup.gasAmount) + OWNER_FEE_RESERVE).toString();
421
451
  }
422
452
  /** Set aside for the owner's own transaction fees when checking what they hold. */
423
453
  export const OWNER_FEE_RESERVE = 50000000n;
package/dist/plugin.js CHANGED
@@ -20,9 +20,9 @@ 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, readHoldings, readStatus, withMarketFacts, } from './chain.js';
24
24
  import { describePolicy, describeSetup, renderRows } from './describe.js';
25
- import { address, coinBySymbol, coinInfo, DAY_MS, defaultGasDaily, DeploymentSchema, display, isStable, limitUnits, poolsFor, recipientName, sameCoin, SetupSchema, showCoin, units, VenueSchema, } from './domain.js';
25
+ import { address, coinBySymbol, coinInfo, DAY_MS, defaultGasDaily, DeploymentSchema, display, isStable, limitUnits, poolsFor, recipientName, requiredSui, sameCoin, SetupSchema, showCoin, SUI, uncovered, units, VenueSchema, } from './domain.js';
26
26
  import { passesByAgent } from './explorer.js';
27
27
  import { passEnded, returnGasFloat } from './float.js';
28
28
  import { failure, INSTRUCTIONS, registerDocs, result, serverInfo, sessionInfo, SIGN_ANNOTATIONS, SIGN_DESCRIPTION, } from './mcp.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.',
@@ -469,8 +471,40 @@ export function createPluginMcp(registry) {
469
471
  networks: [...own, ...bundled.filter((b) => !own.some((o) => o.network === b.network))],
470
472
  });
471
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
+ });
472
506
  server.registerTool('propose_authorization', {
473
- 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.",
474
508
  inputSchema: {
475
509
  name: z
476
510
  .string()
@@ -495,9 +529,12 @@ export function createPluginMcp(registry) {
495
529
  .string()
496
530
  .optional()
497
531
  .describe('path of a private deployment.json; overrides network'),
532
+ owner: address
533
+ .optional()
534
+ .describe("the owner's address, when they gave it: adds warnings where the caps reach past what they hold, or when they cannot sign yet"),
498
535
  },
499
536
  annotations: { readOnlyHint: false, destructiveHint: false },
500
- }, async ({ deployment, network, ...input }) => {
537
+ }, async ({ deployment, network, owner, ...input }) => {
501
538
  try {
502
539
  const d0 = await registry.deployment(deployment, network);
503
540
  const { setup, d } = await toSetup(clientFor(d0), d0, input);
@@ -523,6 +560,17 @@ export function createPluginMcp(registry) {
523
560
  warnings.push(`DeepBook takes its fee from the input and fills nothing under its minimum order, so a sale needs at least ${showCoin(d, sw.coinIn, floor)} and this pass allows ${showCoin(d, sw.coinIn, sw.perTrade)}: no sale could ever happen. Propose again with Cetus added to venues or a larger perTrade, unless the owner wants exactly this.`);
524
561
  }
525
562
  }
563
+ if (owner) {
564
+ const holdings = await readHoldings(clientFor(d), d, owner).catch(() => null);
565
+ if (holdings) {
566
+ const sui = holdings[SUI];
567
+ const total = BigInt(sui?.addressBalance ?? '0') + BigInt(sui?.coinBalance ?? '0');
568
+ if (total < BigInt(requiredSui(setup)))
569
+ warnings.push(`The owner holds ${showCoin(d, SUI, total.toString())} and signing needs ${showCoin(d, SUI, requiredSui(setup))} for the agent's gas stipend and the fee; they must add SUI before they can sign.`);
570
+ for (const [coin, { drawable: most, covered }] of Object.entries(uncovered(setup, holdings, d.demoFaucet?.coin ?? null)))
571
+ warnings.push(`The rules can draw up to ${showCoin(d, coin, most)} but the owner's address balance will hold ${showCoin(d, coin, covered)} after signing. Signing still works; each action runs only while the balance covers it, and swap proceeds that land there count. Tell the owner before they sign.`);
572
+ }
573
+ }
526
574
  return result({
527
575
  pass: session.id,
528
576
  sessionAddress: session.store.state.sessionAddress,
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
@@ -79,19 +82,25 @@ ask for something like "restake with a better validator".
79
82
  to the agent key, outside the rules. `gasCapSui` (default 0.1) is a separate allowance the key
80
83
  draws on to top up its gas, and `gasDailySui` (default a fifth of `gasCapSui`) its daily limit.
81
84
 
82
- **What the owner needs.** Allowances draw only from the owner's address balance, never from coin
83
- objects. The review page reads both, and the signature converts coin objects into the address
84
- balance as far as each cap (plus `gasCapSui` for SUI) needs; the converted coins stay theirs. The
85
- owner needs that much in total, address balance and coin objects together, plus `gasSui` and fees.
85
+ **What the owner needs.** Signing moves only the agent's gas stipend and pays the fee, so that is
86
+ all the owner must hold, in SUI. The caps are limits on what the rules may draw later, not money
87
+ needed now. Allowances draw only from the owner's address balance, never from coin objects; the
88
+ signature converts the owner's coin objects into that balance as far as each cap needs and they
89
+ have them, and the converted coins stay theirs. When a cap reaches past what the owner holds,
90
+ signing still works: each action runs while the balance covers it, and swap proceeds landing
91
+ there count right away, which is how a chain like TAKE to SUI to USDC funds its later legs. Pass
92
+ `owner` to `propose_authorization` when you know the address, and tell the owner about any
93
+ `warnings` it returns before they sign. An action refused because the balance was short is not
94
+ one of the owner's rules; say so.
86
95
 
87
96
  Before you call anything, show the owner the rules you intend to propose as one short list and
88
97
  ask for a yes, unless they already stated every rule explicitly.
89
98
 
90
99
  ## 2. Propose the pass and wait for the signature
91
100
 
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`.
101
+ `propose_authorization`, `propose_change`, `list_passes`, `find_pools`, `revoke_pass` and
102
+ `forget_pass` live on the stdio server (`agent-pass mcp`), which keeps every session on the
103
+ machine. A per-session HTTP signer has only `session_info` and `sign_transaction`.
95
104
 
96
105
  The pass goes on the network the owner names (`network`, one of `list_passes.networks`), else on
97
106
  the first network listed. A private publish of one's own takes a `deployment` path instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/sui-agent-pass",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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
@@ -84,19 +87,25 @@ ask for something like "restake with a better validator".
84
87
  to the agent key, outside the rules. `gasCapSui` (default 0.1) is a separate allowance the key
85
88
  draws on to top up its gas, and `gasDailySui` (default a fifth of `gasCapSui`) its daily limit.
86
89
 
87
- **What the owner needs.** Allowances draw only from the owner's address balance, never from coin
88
- objects. The review page reads both, and the signature converts coin objects into the address
89
- balance as far as each cap (plus `gasCapSui` for SUI) needs; the converted coins stay theirs. The
90
- owner needs that much in total, address balance and coin objects together, plus `gasSui` and fees.
90
+ **What the owner needs.** Signing moves only the agent's gas stipend and pays the fee, so that is
91
+ all the owner must hold, in SUI. The caps are limits on what the rules may draw later, not money
92
+ needed now. Allowances draw only from the owner's address balance, never from coin objects; the
93
+ signature converts the owner's coin objects into that balance as far as each cap needs and they
94
+ have them, and the converted coins stay theirs. When a cap reaches past what the owner holds,
95
+ signing still works: each action runs while the balance covers it, and swap proceeds landing
96
+ there count right away, which is how a chain like TAKE to SUI to USDC funds its later legs. Pass
97
+ `owner` to `propose_authorization` when you know the address, and tell the owner about any
98
+ `warnings` it returns before they sign. An action refused because the balance was short is not
99
+ one of the owner's rules; say so.
91
100
 
92
101
  Before you call anything, show the owner the rules you intend to propose as one short list and
93
102
  ask for a yes, unless they already stated every rule explicitly.
94
103
 
95
104
  ## 2. Propose the pass and wait for the signature
96
105
 
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`.
106
+ `propose_authorization`, `propose_change`, `list_passes`, `find_pools`, `revoke_pass` and
107
+ `forget_pass` live on the stdio server (`agent-pass mcp`), which keeps every session on the
108
+ machine. A per-session HTTP signer has only `session_info` and `sign_transaction`.
100
109
 
101
110
  The pass goes on the network the owner names (`network`, one of `list_passes.networks`), else on
102
111
  the first network listed. A private publish of one's own takes a `deployment` path instead.