opindex-agent 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ An agent's own Solana wallet, token risk answers it pays for over x402, and
4
4
  swaps it signs itself.
5
5
 
6
6
  Two ways in. `src/` is a library for an agent that does not speak MCP; `mcp/`
7
- is the same library behind seven tools for a host that does.
7
+ is the same library behind ten tools for a host that does.
8
8
 
9
9
  ## Install (as an MCP server)
10
10
 
@@ -14,14 +14,19 @@ No configuration is required. Add it to your agent host's MCP config:
14
14
  {
15
15
  "mcpServers": {
16
16
  "opindex": {
17
- "command": "node",
18
- "args": ["/absolute/path/to/opindex-agent/mcp/server.mjs"]
17
+ "command": "npx",
18
+ "args": ["-y", "opindex-agent"]
19
19
  }
20
20
  }
21
21
  }
22
22
  ```
23
23
 
24
- Run `npm install` in this directory first.
24
+ That is the whole installation — `npx` fetches the package on first run. To work
25
+ from a clone instead, point `command` at `node` and `args` at
26
+ `/absolute/path/to/opindex-agent/mcp/server.mjs`, and run `npm install` here first.
27
+
28
+ Then ask your agent to call `agent_wallet`. The key is created on first use, and
29
+ the answer carries the address to fund.
25
30
 
26
31
  The skill in `skills/opindex-trading/SKILL.md` teaches an agent the order to
27
32
  use the tools in. Load it the way your host loads skills; without it the tool
package/mcp/server.mjs CHANGED
@@ -50,6 +50,7 @@ const USDC = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
50
50
  const PUBLIC_RPC = 'https://api.mainnet-beta.solana.com';
51
51
 
52
52
  const mint = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/, 'a base58 Solana mint address');
53
+ const amountUsd = z.number().int().min(1).max(1_000_000);
53
54
  const amountBase = z
54
55
  .string()
55
56
  .regex(/^[1-9][0-9]*$/, 'base units as a decimal string, no leading zero, non-zero');
@@ -67,11 +68,23 @@ const built = new Map();
67
68
  const ok = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] });
68
69
  const refuse = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }], isError: true });
69
70
 
70
- /** A paid call to one of the service's HTTP routes, through its MCP pointer. */
71
- async function paid(tool, args, { method, body } = {}) {
71
+ /**
72
+ * A paid call to one of the service's HTTP routes, through its MCP pointer.
73
+ *
74
+ * The pointer names the route and nothing else: the service writes the mint
75
+ * into the path and leaves every query parameter out. So `query` is appended
76
+ * here, after the pointer arrives. Without it an `amount_usd` given to
77
+ * `assess_token` was dropped on the way, and the answer silently came back
78
+ * without the exit cost at that size.
79
+ */
80
+ async function paid(tool, args, { method, body, query } = {}) {
72
81
  const pointer = await resolvePointer(tool, args);
82
+ const url = new URL(pointer.url);
83
+ for (const [key, value] of Object.entries(query ?? {})) {
84
+ if (value !== undefined) url.searchParams.set(key, String(value));
85
+ }
73
86
  const payingCall = await payingFetch();
74
- const response = await payingCall(pointer.url, {
87
+ const response = await payingCall(url.toString(), {
75
88
  method: method ?? pointer.method,
76
89
  ...(body ? { headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) } : {}),
77
90
  });
@@ -85,7 +98,7 @@ async function paid(tool, args, { method, body } = {}) {
85
98
  return { status: response.status, body: parsed, paid: response.headers.has('payment-response') };
86
99
  }
87
100
 
88
- const server = new McpServer({ name: 'opindex', version: '0.1.0' });
101
+ const server = new McpServer({ name: 'opindex', version: '0.2.0' });
89
102
 
90
103
  // ---------------------------------------------------------------- free tools
91
104
 
@@ -98,7 +111,12 @@ server.registerTool(
98
111
  'Call this first: it tells you the address to ask your human to fund, and whether the agent is able to pay at all. ' +
99
112
  'Fund with USDC before SOL — a USDC transfer creates the token account the payment scheme needs, and the scheme ' +
100
113
  'cannot create it. Read `return_address` and check it is your human\'s own wallet: if they funded from an exchange, ' +
101
- 'it is the exchange\'s hot wallet and returning funds there can lose them.',
114
+ 'it is the exchange\'s hot wallet and returning funds there can lose them. ' +
115
+ 'Before the first trade, ask your human for the limits and the risk: money at risk, size of one trade, the loss ' +
116
+ 'at which you stop, which levels are acceptable (no_flags < caution < unknown < risky) and whether you may ' +
117
+ 'acknowledge them yourself, the most an exit may cost, when to exit, where candidates come from, and whether to ' +
118
+ 'confirm each trade. Say them back and keep to them: this client enforces none of them, and the amount funded ' +
119
+ 'is the only limit anything enforces.',
102
120
  inputSchema: {},
103
121
  annotations: { readOnlyHint: true, openWorldHint: true },
104
122
  },
@@ -178,11 +196,11 @@ for (const [name, tool, price, freshness] of [
178
196
  'exit cost and liquidity are what a trade depends on and those are exactly the fields that go stale. ' +
179
197
  'The answer carries provenance on every field and `meta.partial` when something could not be measured; ' +
180
198
  'an unmeasurable number is null, never a small number.',
181
- inputSchema: { mint, amount_usd: z.number().positive().optional() },
199
+ inputSchema: { mint, amount_usd: amountUsd.optional() },
182
200
  annotations: { readOnlyHint: true, openWorldHint: true },
183
201
  },
184
202
  async ({ mint: m, amount_usd }) => {
185
- const { status, body, paid: wasPaid } = await paid(tool, { mint: m, ...(amount_usd ? { amount_usd } : {}) });
203
+ const { status, body, paid: wasPaid } = await paid(tool, { mint: m }, { query: { amount_usd } });
186
204
  if (status !== 200) return refuse({ status, charged: wasPaid, ...body });
187
205
  return ok({ ...body, _charged: wasPaid ? price : 'not charged' });
188
206
  },
@@ -204,7 +222,9 @@ server.registerTool(
204
222
  ' not_built — a named reason; no transaction exists.\n' +
205
223
  ' unassessable — the risk could not be established, so nothing was built and nothing was charged.\n' +
206
224
  'The raw transaction is deliberately not returned: `submit_swap` takes the `swap_id` so that no transaction from ' +
207
- 'anywhere else can be submitted through this agent.',
225
+ 'anywhere else can be submitted through this agent.\n' +
226
+ 'Build only within the limits your human set (see `agent_wallet`). If none were set, ask for them before ' +
227
+ 'building: this costs money on every call, refusals included.',
208
228
  inputSchema: {
209
229
  input_mint: mint,
210
230
  output_mint: mint,
@@ -236,6 +256,95 @@ server.registerTool(
236
256
  },
237
257
  );
238
258
 
259
+ // ---------------------------------------------------------- trading data
260
+ //
261
+ // Three routes served from the service's own samples of the tokens it watches.
262
+ // Each answer says how it was sampled and how much of the market it covers;
263
+ // none of them says what to buy.
264
+ const samplesNote =
265
+ 'Served from the service\'s own samples of the chain, about once a minute, not from a transaction feed. ' +
266
+ '`404 NO_HISTORY` means not sampled yet: free, ask again in a minute. `404 NOT_WATCHED` is free too; do not loop on ' +
267
+ 'it. Data only, not investment advice.';
268
+
269
+ /** One of the three data routes: paid, and a free 404 is said to be free. */
270
+ async function sampled(tool, args, query, price) {
271
+ const { status, body, paid: wasPaid } = await paid(tool, args, { query });
272
+ if (status !== 200) return refuse({ status, charged: wasPaid, ...body });
273
+ return ok({ ...body, _charged: wasPaid ? price : 'not charged' });
274
+ }
275
+
276
+ server.registerTool(
277
+ 'screen_tokens',
278
+ {
279
+ title: 'Watched tokens that meet your conditions ($0.02, paid from the wallet)',
280
+ description:
281
+ 'PAID: $0.02 in USDC from this agent\'s wallet, an empty list included. The tokens this service watches that pass ' +
282
+ 'the conditions YOU give, each with why it is included. `meta.coverage` says how many are watched: the list is ' +
283
+ 'not the whole market, and there is no ranking by anyone\'s opinion; `sort` is one measurable field. Use your ' +
284
+ 'human\'s limits as the conditions: `max_level`, `max_exit_cost_bps` at `amount_usd`, `min_liquidity_usd`. ' +
285
+ 'A condition whose value could not be measured is not met. ' +
286
+ samplesNote,
287
+ inputSchema: {
288
+ max_level: z.enum(['no_flags', 'caution']).optional(),
289
+ min_liquidity_usd: z.number().positive().optional(),
290
+ max_exit_cost_bps: z.number().int().nonnegative().optional().describe('needs amount_usd'),
291
+ amount_usd: amountUsd.optional(),
292
+ min_age_hours: z.number().int().nonnegative().optional(),
293
+ quote: z.enum(['sol', 'usdc', 'any']).optional(),
294
+ sort: z
295
+ .enum(['liquidity', 'price_change_1h', 'price_change_24h', 'flow_1h', 'liquidity_change_24h', 'curve_progress'])
296
+ .optional(),
297
+ limit: z.number().int().min(1).max(50).optional(),
298
+ cursor: z.string().optional().describe('next_cursor of the previous page, with the same conditions'),
299
+ },
300
+ annotations: { readOnlyHint: true, openWorldHint: true },
301
+ },
302
+ async (query) => sampled('screen_tokens', {}, query, '$0.02'),
303
+ );
304
+
305
+ server.registerTool(
306
+ 'token_history',
307
+ {
308
+ title: 'Sampled price, liquidity and holders of a token ($0.01, paid from the wallet)',
309
+ description:
310
+ 'PAID: $0.01 in USDC from this agent\'s wallet. Candles, liquidity, reserve flow and holder concentration of one ' +
311
+ 'token over a window, made of samples (`sampling.method: reserve_snapshot`): a minute with no sample has no candle. ' +
312
+ '`reserve_flow` is classed swap_like, deposit_like, withdrawal_like or quiet: what the reserves looked like, ' +
313
+ 'never trade volume. A paid call on a token nobody watches puts it on watch. ' +
314
+ samplesNote,
315
+ inputSchema: {
316
+ mint,
317
+ window: z.enum(['1h', '6h', '24h', '7d']).optional(),
318
+ resolution: z.enum(['1m', '5m', '15m', '1h']).optional(),
319
+ pool: mint.optional().describe('one pool of this mint, to read that pool rather than the watched one'),
320
+ },
321
+ annotations: { readOnlyHint: true, openWorldHint: true },
322
+ },
323
+ async ({ mint: m, ...query }) => sampled('token_history', { mint: m }, query, '$0.01'),
324
+ );
325
+
326
+ server.registerTool(
327
+ 'token_indicators',
328
+ {
329
+ title: 'Indicators for one token, and your position in it ($0.01, paid from the wallet)',
330
+ description:
331
+ 'PAID: $0.01 in USDC from this agent\'s wallet. Numbers with formulas for one watched token: price and liquidity ' +
332
+ 'change, drawdown, volatility, reserve flow, holder and safety changes, and exit cost at `amount_usd`. Give ' +
333
+ '`entry_price_usd` and `position_base_units` together to get your position; it reads nothing of your wallet. ' +
334
+ 'Read `safety_changes_24h` first when you hold the token. A number that cannot be computed is null, never zero. ' +
335
+ 'This route never puts a token on watch: on `NO_HISTORY` for a token nobody watches, call `token_history` first. ' +
336
+ samplesNote,
337
+ inputSchema: {
338
+ mint,
339
+ amount_usd: amountUsd.optional(),
340
+ entry_price_usd: z.number().positive().optional(),
341
+ position_base_units: amountBase.optional(),
342
+ },
343
+ annotations: { readOnlyHint: true, openWorldHint: true },
344
+ },
345
+ async ({ mint: m, ...query }) => sampled('token_indicators', { mint: m }, query, '$0.01'),
346
+ );
347
+
239
348
  // ------------------------------------------------------------ signing tools
240
349
 
241
350
  server.registerTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opindex-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "An agent's Solana wallet, token risk answers paid for over x402, and swaps it signs itself. Library plus an MCP server.",
5
5
  "license": "MIT",
6
6
  "author": "MONOLITH LABS LTD <info@monolithlabs.uk>",
@@ -5,29 +5,42 @@ description: Use when trading Solana SPL tokens, or when asked whether a token i
5
5
 
6
6
  # Trading Solana tokens through opindex
7
7
 
8
- You have a wallet of your own and seven tools. Three are free, three cost USDC
8
+ You have a wallet of your own and ten tools. Two are free reads, six cost USDC
9
9
  from that wallet, and two move money. This explains the order to use them in
10
10
  and the things that will cost your human money if you get them wrong.
11
11
 
12
12
  ## What this service does not answer
13
13
 
14
14
  Before anything else, so you do not build a plan on it: **this service does not
15
- tell you what to buy or when.**
16
-
17
- - **No discovery.** You must already know the mint. There is no screener, no
18
- ranking, nothing that says what is moving.
19
- - **No history.** No candles, no volume, no price change. Every market field is
20
- a single point in time, so nothing here shows direction.
21
- - **No position.** It does not know what you hold, what you paid, or what you
22
- are up or down.
23
-
24
- What it is: a filter and a cost model. It tells you which tokens not to touch,
25
- and what leaving one costs at the size you actually hold — which is what sizes
26
- a position, not what picks it. That is a necessary input to a trade and not a
27
- sufficient one, so you need a second source for candidates and for timing.
15
+ tell you what to hold or when to act.** It gives you numbers and says how they
16
+ were measured; the decision is yours.
17
+
18
+ - **Discovery exists only among tokens the service already watches.**
19
+ `screen` lists the watched tokens that pass the conditions *you* give it, and
20
+ `meta.coverage` says how many tokens are watched, how many passed and where
21
+ the set came from. It is not the market: a token missing from a screen may
22
+ simply not be watched. There is no ranking by our opinion; `sort` is one
23
+ measurable field.
24
+ - **History is by samples.** `history` and `indicators` come from readings the
25
+ service takes of a pool about once a minute and of its holders about once an
26
+ hour. `sampling.method` says so: `reserve_snapshot`. A candle is a candle over
27
+ samples. A minute with no sample has no candle. There is no transaction feed
28
+ behind any of it.
29
+ - **Position is by your data.** It does not know what you hold or what you paid.
30
+ Give `indicators` your `entry_price_usd` and `position_base_units` and it will
31
+ compute from those two numbers and nothing else; it reads nothing of your
32
+ wallet.
33
+
34
+ What it is: a filter, a cost model and a record of how a pool's reserves moved.
35
+ It tells you which tokens not to touch, what leaving one costs at the size you
36
+ actually hold, and what has changed since you looked. That is an input to a
37
+ decision and not the decision, and not a source of candidates beyond the watched
38
+ set or of timing beyond the samples.
28
39
 
29
40
  If your human expects you to trade profitably using only this, say so plainly
30
- before you start rather than after.
41
+ before you start rather than after. Then do not stop there: if they still want
42
+ you to trade, the decision is theirs to delegate, and the next step is agreeing
43
+ the limits with them (below), not declining.
31
44
 
32
45
  ## Start with the wallet, before anything else
33
46
 
@@ -48,6 +61,38 @@ Two things in its answer matter more than the balances:
48
61
  You need **no SOL to pay for answers** — the service's facilitator pays that
49
62
  network fee. You need SOL only for swaps you sign yourself.
50
63
 
64
+ ## Before the first trade, agree the limits and the risk
65
+
66
+ Funding you is not a decision about what to do with the money. Before your first
67
+ trade, ask your human for the following, and do not trade until they have
68
+ answered. Ask only for what they have not already said. If they say "you
69
+ decide", propose numbers yourself and have them confirm: a limit they confirmed
70
+ is theirs, one you assumed is not.
71
+
72
+ 1. **Money at risk.** All of the wallet, or how much of it.
73
+ 2. **Size of one trade.** In USD, or as a share of the money at risk.
74
+ 3. **When to stop.** The loss on the money at risk after which you stop trading
75
+ and report, instead of trading to win it back.
76
+ 4. **Which risk levels are acceptable**, in this service's words: only
77
+ `no_flags`; or `caution` too, and then whether any factor is still off limits
78
+ (`freeze_authority_active`, say); `unknown` and `risky` only if they name
79
+ them. And whether you may acknowledge an allowed level yourself, or must ask
80
+ each time.
81
+ 5. **Exit cost.** The most leaving may cost at the trade's size
82
+ (`exit_cost_for_amount`) before you will not enter. `null` is unknown and
83
+ fails this check.
84
+ 6. **When to exit a position.** A gain to take, a loss to cut, or a time after
85
+ which you leave anyway.
86
+ 7. **Where candidates come from.** Only mints they give you, or also ones you
87
+ find, and from where.
88
+ 8. **Confirmation.** Ask before each trade, or trade within these limits and
89
+ report after.
90
+
91
+ Say the answers back as one short list and keep to it. **These limits are yours
92
+ to keep: the client enforces none of them.** The amount funded is the only limit
93
+ anything enforces, so tell your human that too. If a trade would break a limit,
94
+ do not make it; say which limit and ask. Changing a limit is their call.
95
+
51
96
  ## Never trade on a free answer
52
97
 
53
98
  `token_identity` is free and may be up to a day stale. That is fine for "what is
@@ -62,15 +107,22 @@ half-cent.
62
107
 
63
108
  ## Read the verdict properly, not just the level
64
109
 
65
- `heuristic.level` is `no_flags`, `caution`, `unknown` or `risky`. **Read
110
+ `heuristic.level` is `no_flags`, `caution`, `unknown` or `risky`; a factor's `severity` is `risky`, `caution` or `info`. **Read
66
111
  `heuristic.factors` even when the level is calm.** A trusted issuer gets a quiet
67
112
  level while the facts still say what they say: USDC comes back `no_flags` with
68
113
  `mint_authority_active` and `freeze_authority_active` both marked risky, because
69
114
  its issuer really can mint and really can freeze your account. The level is not
70
115
  a promise; the factors are the evidence.
71
116
 
72
- Two more habits worth having:
117
+ Three more habits worth having:
73
118
 
119
+ - **`info` is a fact that, for a mature token, does not raise the level.** Read
120
+ it if you hold a position: `lp_not_burned`, `metadata_mutable` and
121
+ `holder_concentration` still say what they say. `heuristic.maturity` shows
122
+ why the token counted as mature (liquidity, age since this service first saw
123
+ it, and who holds its LP). A token not yet mature, including every token in
124
+ the first 30 days of the service seeing it, keeps these as `caution`. The
125
+ default `screen` filter `max_level=no_flags` now lists mature tokens.
74
126
  - **`unknown` outranks `caution`.** An unnamed gap can hide anything, so treat
75
127
  it as worse than a named mild finding, never better.
76
128
  - **A null is not a small number.** `null` means the service could not measure
@@ -98,10 +150,77 @@ price of the assessment behind it, not a bug. So read the factors before you
98
150
  re-send: if you are going to decline the trade anyway, decline it now rather
99
151
  than paying twice to arrive there.
100
152
 
101
- **Before you acknowledge, tell your human what you are accepting.** Name the
102
- factors, not the level. "This token's LP is not burned and a transfer hook can
153
+ **Before you acknowledge, tell your human what you are accepting**, unless they
154
+ already allowed that level in the limits you agreed; then acknowledge and name
155
+ the factors in your report. Either way, name the factors, not the level. "This token's LP is not burned and a transfer hook can
103
156
  still be set on it" is a sentence they can judge. "Level: caution" is not.
104
157
 
158
+ ## How to read `*_like`
159
+
160
+ `history.reserve_flow` and `indicators.flow` classify each interval between two
161
+ samples by the signs of how both vaults of the pool moved:
162
+
163
+ - **`swap_like`** — the two moved in opposite directions. At least one swap
164
+ happened in that minute, and its size is a **net**, not a turnover.
165
+ - **`deposit_like`** — neither shrank (both deltas ≥ 0, not both zero). Looks
166
+ like liquidity added.
167
+ - **`withdrawal_like`** — neither grew (both deltas ≤ 0, not both zero). Looks
168
+ like liquidity removed, and may hide swaps.
169
+ - **`quiet`** — neither moved.
170
+
171
+ The suffix is the point. A minute can hold a swap and a deposit together, so the
172
+ class says what the reserves *looked like*, never what happened. The note on the
173
+ field says the same: deltas of the quote vault, classified by the signs of both
174
+ vault deltas, **not trade volume**. Never turn `quote_net` into a count of
175
+ transactions or a size of activity, and never report it as one.
176
+
177
+ A hole in the samples is not a `quiet` pool: across a gap no interval exists.
178
+ `sampling.gaps` counts them, and a number that could not be measured is `null`
179
+ with `meta.partial: true`, never zero.
180
+
181
+ ## `404 NO_HISTORY` is free: wait a minute
182
+
183
+ `history` and `indicators` answer `404 NO_HISTORY` when the service has fewer than
184
+ two samples of that token. It is a client error, **it is not charged**, and it
185
+ is not a failure: it is the start of an answer.
186
+
187
+ - `history` on a token nobody watches asks the service to start watching it,
188
+ when the request was a paying one. Ask again in a minute.
189
+ - `indicators` never starts a watch. If it says `NO_HISTORY` for a token that
190
+ nobody watches, call `history` first, then `indicators`.
191
+ - `404 NOT_WATCHED` means the pool you named is not that token's watched pool, or
192
+ its watch has ended. Asking again does not change that; do not loop on it.
193
+ - `404 NOT_WATCHED` from all three routes, `screen` included, with "the watcher
194
+ is not running on this deployment" means that deployment collects nothing.
195
+ It is free; do not retry it there.
196
+
197
+ Do not retry faster than once a minute, and do not read a `NO_HISTORY` as "this
198
+ token has no history": it means "not yet sampled".
199
+
200
+ ## Indicators are numbers, not advice
201
+
202
+ Every answer ends `"disclaimer": "Data only, not investment advice."`, and it
203
+ means it. There is no rating, no action field and no ranking by our opinion.
204
+ What there is, is a number with a formula (`docs/API.md`, "The trading-data
205
+ routes") that you can recompute.
206
+
207
+ - Say what the number is and how it was measured when you report it to your
208
+ human: "liquidity fell 18% in one sample interval, which looks like a
209
+ withdrawal", not "the pool is being rugged".
210
+ - `price.change` is over *sampled closes* and is `null` when there is no sample
211
+ of the right age. A `null` is not "no change".
212
+ - `exit.cost_now_bps_at_amount` on a bonding curve is `null` (`basis:
213
+ curve_not_sampled`): the curve's exit is not costed from samples. Ask
214
+ `assess_token_live` when you need a cost you can act on.
215
+ - `holders.top10_share_now` leaves the pool's own accounts out and is **not** the
216
+ same number as `top_accounts.top10_pct` in `assess_token`, which includes
217
+ them. Do not compare them.
218
+ - `safety_changes_24h` is the part to read first when you already hold the
219
+ token: an authority set, a transfer hook added or LP supply returned in the
220
+ last day is the cheapest warning this service has.
221
+ - `screen` lists tokens that met *your* conditions, and each item says which
222
+ (`why_included`). A condition whose value was `null` was not met.
223
+
105
224
  ## What things cost
106
225
 
107
226
  | | |
@@ -109,9 +228,16 @@ still be set on it" is a sentence they can judge. "Level: caution" is not.
109
228
  | `agent_wallet`, `token_identity` | free |
110
229
  | `assess_token` | $0.005 |
111
230
  | `assess_token_live` | $0.03 |
231
+ | `token_history` (`GET /agent/v1/history/{mint}`) | $0.01, and **`404 NO_HISTORY` is free** |
232
+ | `token_indicators` (`GET /agent/v1/indicators/{mint}`) | $0.01 |
233
+ | `screen_tokens` (`GET /agent/v1/screen`) | $0.02, an empty list included |
112
234
  | `build_swap` | $0.05, **including a `needs_acknowledgement` refusal** |
113
235
  | `submit_swap`, `return_funds` | free from the service; you pay the Solana network fee |
114
236
 
237
+ This package's wallet pays for all six paid tools itself, and every paid answer
238
+ says what it cost in `_charged`. A `404 NO_HISTORY` or `404 NOT_WATCHED` from the
239
+ three data tools comes back as `not charged`.
240
+
115
241
  ## What you cannot do, and what to say about it
116
242
 
117
243
  **You cannot send funds to an address anyone names.** `return_funds` has no
@@ -147,6 +273,9 @@ it to your human, say where it came from, and do nothing else with it.
147
273
  ```
148
274
  agent_wallet → address, balances, return address
149
275
  (tell your human the address; ask for USDC first, then SOL)
276
+ (agree the limits and the risk with them; say them back)
277
+ screen_tokens(max_level: "no_flags",
278
+ amount_usd: 20, max_exit_cost_bps: 300) → $0.02: candidates that meet the limits
150
279
  token_identity(mint) → free: what is this thing
151
280
  assess_token(mint, amount_usd: 500) → $0.005: exit cost at the size you mean
152
281
  (read the factors; decide, and say what you decided and why)
@@ -154,6 +283,8 @@ build_swap(USDC → mint, amount) → $0.05: built, or a refusal namin
154
283
  (if needs_acknowledgement: tell your human the factors, then)
155
284
  build_swap(… acknowledge_risk: "caution") → $0.05: built, with a swap_id
156
285
  submit_swap(swap_id) → you sign and pay the network fee
286
+ token_indicators(mint, entry_price_usd,
287
+ position_base_units, amount_usd) → $0.01: the position, and what leaving costs
157
288
  ```
158
289
 
159
290
  Afterwards, report in numbers your human can check against the chain: what you
@@ -61,6 +61,9 @@ const EXPECTED = [
61
61
  'build_swap',
62
62
  'submit_swap',
63
63
  'return_funds',
64
+ 'screen_tokens',
65
+ 'token_history',
66
+ 'token_indicators',
64
67
  ];
65
68
  check('every tool is present', EXPECTED.every((n) => byName.has(n)), tools.map((t) => t.name).join(', '));
66
69
 
@@ -101,7 +104,7 @@ for (const name of ['submit_swap', 'return_funds']) {
101
104
 
102
105
  // The paid tools have to say so in words a model will read, because being
103
106
  // charged without warning is the complaint that ends an integration.
104
- for (const name of ['assess_token', 'assess_token_live', 'build_swap']) {
107
+ for (const name of ['assess_token', 'assess_token_live', 'build_swap', 'screen_tokens', 'token_history', 'token_indicators']) {
105
108
  check(`${name} says it is paid`, (byName.get(name)?.description ?? '').includes('PAID'), 'description');
106
109
  }
107
110
 
@@ -124,7 +127,14 @@ check('the skill mentions every tool', missingFromSkill.length === 0, missingFro
124
127
  // the skill proves nothing about a document that mentions prices in several
125
128
  // places: a wrong price in the table passes while the right one still stands
126
129
  // in a worked example below.
127
- const PRICES = { assess_token: '$0.005', assess_token_live: '$0.03', build_swap: '$0.05' };
130
+ const PRICES = {
131
+ assess_token: '$0.005',
132
+ assess_token_live: '$0.03',
133
+ build_swap: '$0.05',
134
+ screen_tokens: '$0.02',
135
+ token_history: '$0.01',
136
+ token_indicators: '$0.01',
137
+ };
128
138
  const expectedPrices = new Set(Object.values(PRICES));
129
139
 
130
140
  for (const [tool, price] of Object.entries(PRICES)) {