@potswap_io/mcp 0.1.0 → 0.3.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.
@@ -0,0 +1,72 @@
1
+ export interface HeliusTx {
2
+ signature: string;
3
+ timestamp: number;
4
+ type?: string;
5
+ source?: string;
6
+ fee?: number;
7
+ feePayer?: string;
8
+ transactionError?: unknown;
9
+ nativeTransfers?: {
10
+ fromUserAccount?: string;
11
+ toUserAccount?: string;
12
+ amount?: number;
13
+ }[];
14
+ tokenTransfers?: {
15
+ fromUserAccount?: string;
16
+ toUserAccount?: string;
17
+ tokenAmount?: number;
18
+ mint?: string;
19
+ }[];
20
+ events?: {
21
+ swap?: {
22
+ nativeInput?: {
23
+ account: string;
24
+ amount: string;
25
+ } | null;
26
+ nativeOutput?: {
27
+ account: string;
28
+ amount: string;
29
+ } | null;
30
+ tokenInputs?: {
31
+ userAccount: string;
32
+ mint: string;
33
+ rawTokenAmount: {
34
+ tokenAmount: string;
35
+ decimals: number;
36
+ };
37
+ }[];
38
+ tokenOutputs?: {
39
+ userAccount: string;
40
+ mint: string;
41
+ rawTokenAmount: {
42
+ tokenAmount: string;
43
+ decimals: number;
44
+ };
45
+ }[];
46
+ };
47
+ };
48
+ }
49
+ export declare const SOL = "So11111111111111111111111111111111111111112";
50
+ export interface HistoryItem {
51
+ signature: string;
52
+ time: string;
53
+ kind: 'swap' | 'transfer' | 'other';
54
+ source?: string;
55
+ feeSol: number;
56
+ failed: boolean;
57
+ /** For swaps: what the wallet gave and got. For transfers: amount and direction. */
58
+ inMint?: string;
59
+ inAmount?: number;
60
+ outMint?: string;
61
+ outAmount?: number;
62
+ direction?: 'in' | 'out';
63
+ counterparty?: string;
64
+ mint?: string;
65
+ amount?: number;
66
+ }
67
+ export declare function normalizeHistory(txs: HeliusTx[], wallet: string): HistoryItem[];
68
+ export type TxStatus = 'not found' | 'processed' | 'confirmed' | 'finalized' | 'failed';
69
+ export declare function mapSignatureStatus(s: {
70
+ confirmationStatus?: string;
71
+ err?: unknown;
72
+ } | null): TxStatus;
@@ -0,0 +1,53 @@
1
+ export const SOL = 'So11111111111111111111111111111111111111112';
2
+ export function normalizeHistory(txs, wallet) {
3
+ return txs.map((t) => {
4
+ const base = { signature: t.signature, time: new Date((t.timestamp ?? 0) * 1000).toISOString(), kind: 'other', source: t.source, feeSol: (t.fee ?? 0) / 1e9, failed: !!t.transactionError };
5
+ const sw = t.events?.swap;
6
+ if (sw) {
7
+ // Determine what this wallet put in and took out.
8
+ let inMint, inAmount = 0, outMint, outAmount = 0;
9
+ if (sw.nativeInput && sw.nativeInput.account === wallet) {
10
+ inMint = SOL;
11
+ inAmount = Number(sw.nativeInput.amount) / 1e9;
12
+ }
13
+ if (sw.nativeOutput && sw.nativeOutput.account === wallet) {
14
+ outMint = SOL;
15
+ outAmount = Number(sw.nativeOutput.amount) / 1e9;
16
+ }
17
+ for (const i of sw.tokenInputs ?? [])
18
+ if (i.userAccount === wallet) {
19
+ inMint = i.mint;
20
+ inAmount += Number(i.rawTokenAmount.tokenAmount) / 10 ** i.rawTokenAmount.decimals;
21
+ }
22
+ for (const o of sw.tokenOutputs ?? [])
23
+ if (o.userAccount === wallet) {
24
+ outMint = o.mint;
25
+ outAmount += Number(o.rawTokenAmount.tokenAmount) / 10 ** o.rawTokenAmount.decimals;
26
+ }
27
+ if (inMint || outMint)
28
+ return { ...base, kind: 'swap', inMint, inAmount, outMint, outAmount };
29
+ }
30
+ const tt = (t.tokenTransfers ?? []).find((x) => x.fromUserAccount === wallet || x.toUserAccount === wallet);
31
+ if (tt && tt.mint && tt.tokenAmount) {
32
+ const out = tt.fromUserAccount === wallet;
33
+ return { ...base, kind: 'transfer', direction: out ? 'out' : 'in', counterparty: out ? tt.toUserAccount : tt.fromUserAccount, mint: tt.mint, amount: tt.tokenAmount };
34
+ }
35
+ const nt = (t.nativeTransfers ?? []).find((x) => (x.fromUserAccount === wallet || x.toUserAccount === wallet) && (x.amount ?? 0) > 0);
36
+ if (nt) {
37
+ const out = nt.fromUserAccount === wallet;
38
+ return { ...base, kind: 'transfer', direction: out ? 'out' : 'in', counterparty: out ? nt.toUserAccount : nt.fromUserAccount, mint: SOL, amount: (nt.amount ?? 0) / 1e9 };
39
+ }
40
+ return base;
41
+ });
42
+ }
43
+ export function mapSignatureStatus(s) {
44
+ if (!s)
45
+ return 'not found';
46
+ if (s.err)
47
+ return 'failed';
48
+ if (s.confirmationStatus === 'finalized')
49
+ return 'finalized';
50
+ if (s.confirmationStatus === 'confirmed')
51
+ return 'confirmed';
52
+ return 'processed';
53
+ }
package/dist/index.js CHANGED
@@ -1,11 +1,29 @@
1
1
  #!/usr/bin/env node
2
- // Potswap MCP server over stdio. Configure via env (see README).
2
+ // Potswap MCP server over stdio, or `--daemon` to run strategies headlessly (PRD P11.11).
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { createServer, VERSION } from './server.js';
5
5
  import { loadConfig } from './config.js';
6
6
  import { log } from './redact.js';
7
+ import { createRuntime } from './runtime.js';
7
8
  const config = loadConfig();
8
- const server = createServer({ config });
9
- const transport = new StdioServerTransport();
10
- await server.connect(transport);
11
- log(`potswap-mcp v${VERSION} ready · api=${config.apiBase} · maxTradeSol=${config.maxTradeSol} · maxSlippageBps=${config.maxSlippageBps}${config.dryRun ? ' · DRY RUN' : ''}${config.privateKey || config.keypairPath ? '' : ' · no wallet (read-only)'}`);
9
+ const daemon = process.argv.includes('--daemon');
10
+ const rt = createRuntime(config);
11
+ if (daemon) {
12
+ if (!rt.scheduler) {
13
+ log(`potswap-mcp v${VERSION}: another scheduler (pid ${rt.lockOwner}) already runs on ${rt.store.location}; exiting.`);
14
+ process.exit(2);
15
+ }
16
+ rt.scheduler.start();
17
+ log(`potswap-mcp v${VERSION} daemon · state=${rt.store.location} · maxTradeSol=${config.maxTradeSol} · maxDailySol=${config.maxDailySol}${config.dryRun ? ' · DRY RUN' : ''}${rt.signer ? ` · wallet=${rt.signer.publicKey}` : ' · no wallet (strategies cannot execute)'}`);
18
+ const shutdown = async () => { log('stopping after the in-flight leg…'); await rt.scheduler.stop(); rt.release(); process.exit(0); };
19
+ process.on('SIGINT', shutdown);
20
+ process.on('SIGTERM', shutdown);
21
+ }
22
+ else {
23
+ const server = createServer({ config, signer: rt.signer, strategyStore: rt.store, scheduler: rt.scheduler ?? undefined });
24
+ rt.scheduler?.start();
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
27
+ log(`potswap-mcp v${VERSION} ready · api=${config.apiBase} · maxTradeSol=${config.maxTradeSol} · maxSlippageBps=${config.maxSlippageBps}${config.dryRun ? ' · DRY RUN' : ''}${rt.signer ? '' : ' · no wallet (read-only)'}${rt.scheduler ? ' · scheduler on' : ` · scheduler owned by pid ${rt.lockOwner}`}`);
28
+ process.on('exit', () => rt.release());
29
+ }
package/dist/pnl.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { HistoryItem } from './history.js';
2
+ export interface Position {
3
+ mint: string;
4
+ amount: number;
5
+ avgCostUsd?: number;
6
+ costBasisUsd?: number;
7
+ valueUsd?: number;
8
+ unrealisedUsd?: number;
9
+ realisedUsd: number;
10
+ }
11
+ /**
12
+ * Walks swaps oldest→newest. A swap "buys" its outMint at (inAmount × price(inMint at the time)) and
13
+ * "sells" its inMint. Price history is approximated with current prices unless the item carries `inUsd`.
14
+ * Good enough for an agent dashboard; exact fills come from the on-chain record.
15
+ */
16
+ export declare function computePositions(history: HistoryItem[], balances: Record<string, number>, prices: Record<string, number>, quoteMints?: Set<string>): Position[];
package/dist/pnl.js ADDED
@@ -0,0 +1,45 @@
1
+ import { SOL } from './history.js';
2
+ /**
3
+ * Walks swaps oldest→newest. A swap "buys" its outMint at (inAmount × price(inMint at the time)) and
4
+ * "sells" its inMint. Price history is approximated with current prices unless the item carries `inUsd`.
5
+ * Good enough for an agent dashboard; exact fills come from the on-chain record.
6
+ */
7
+ export function computePositions(history, balances, prices, quoteMints = new Set([SOL])) {
8
+ const lots = {};
9
+ const items = [...history].filter((h) => h.kind === 'swap' && !h.failed).sort((a, b) => a.time.localeCompare(b.time));
10
+ for (const h of items) {
11
+ const inUsd = h.inUsd ?? (h.inMint && prices[h.inMint] ? (h.inAmount ?? 0) * prices[h.inMint] : undefined);
12
+ // sell side
13
+ if (h.inMint && !quoteMints.has(h.inMint) && h.inAmount) {
14
+ const l = (lots[h.inMint] ??= { amount: 0, cost: 0, realised: 0 });
15
+ const avg = l.amount > 0 ? l.cost / l.amount : 0;
16
+ const sold = Math.min(h.inAmount, l.amount);
17
+ const proceeds = h.outMint && prices[h.outMint] ? (h.outAmount ?? 0) * prices[h.outMint] : inUsd ?? 0;
18
+ l.realised += proceeds - sold * avg;
19
+ l.amount -= sold;
20
+ l.cost -= sold * avg;
21
+ if (l.amount < 1e-12) {
22
+ l.amount = 0;
23
+ l.cost = 0;
24
+ }
25
+ }
26
+ // buy side
27
+ if (h.outMint && !quoteMints.has(h.outMint) && h.outAmount && inUsd !== undefined) {
28
+ const l = (lots[h.outMint] ??= { amount: 0, cost: 0, realised: 0 });
29
+ l.amount += h.outAmount;
30
+ l.cost += inUsd;
31
+ }
32
+ }
33
+ const mints = new Set([...Object.keys(balances), ...Object.keys(lots)].filter((m) => !quoteMints.has(m)));
34
+ return [...mints].map((mint) => {
35
+ const amount = balances[mint] ?? 0;
36
+ const l = lots[mint];
37
+ const avg = l && l.amount > 0 ? l.cost / l.amount : undefined;
38
+ const price = prices[mint];
39
+ const value = price !== undefined ? amount * price : undefined;
40
+ return {
41
+ mint, amount, avgCostUsd: avg, costBasisUsd: avg !== undefined ? amount * avg : undefined, valueUsd: value,
42
+ unrealisedUsd: value !== undefined && avg !== undefined ? value - amount * avg : undefined, realisedUsd: l?.realised ?? 0,
43
+ };
44
+ }).filter((p) => p.amount > 0 || p.realisedUsd !== 0).sort((a, b) => (b.valueUsd ?? 0) - (a.valueUsd ?? 0));
45
+ }
@@ -0,0 +1,15 @@
1
+ import type { Config } from './config.js';
2
+ import { type Signer } from './wallet.js';
3
+ import { type StrategyStore } from './strategies/store.js';
4
+ import { Scheduler } from './strategies/scheduler.js';
5
+ import type { BalancesFn, TradeContext } from './trade.js';
6
+ export interface Runtime {
7
+ signer: Signer | null;
8
+ store: StrategyStore;
9
+ scheduler: Scheduler | null;
10
+ lockOwner: number | null;
11
+ trade: TradeContext;
12
+ release(): void;
13
+ }
14
+ export declare function createRuntime(config: Config): Runtime;
15
+ export declare function rpcBalances(rpcUrl: string): BalancesFn;
@@ -0,0 +1,39 @@
1
+ // Wires config → signer, API, balances, strategy store, lock and scheduler. Used by index.ts (both modes).
2
+ import { Connection, PublicKey } from '@solana/web3.js';
3
+ import { PotswapApi } from './api.js';
4
+ import { loadSigner } from './wallet.js';
5
+ import { FileStore } from './strategies/store.js';
6
+ import { acquireLock } from './strategies/lock.js';
7
+ import { Scheduler } from './strategies/scheduler.js';
8
+ export function createRuntime(config) {
9
+ const signer = loadSigner({ privateKey: config.privateKey, keypairPath: config.keypairPath, stateDir: config.stateDir });
10
+ const api = new PotswapApi(config.apiBase);
11
+ const balances = rpcBalances(config.rpcUrl);
12
+ const trade = { api, config, signer, balances };
13
+ const dir = config.stateDir;
14
+ const store = new FileStore(dir);
15
+ const lock = acquireLock(dir);
16
+ const scheduler = lock.acquired ? new Scheduler({ store, trade, maxDailySol: config.maxDailySol }) : null;
17
+ return { signer, store, scheduler, lockOwner: lock.ownerPid, trade, release: () => lock.release() };
18
+ }
19
+ export function rpcBalances(rpcUrl) {
20
+ const SOL = 'So11111111111111111111111111111111111111112';
21
+ return async (wallet) => {
22
+ const conn = new Connection(rpcUrl, 'confirmed');
23
+ const owner = new PublicKey(wallet);
24
+ const out = {};
25
+ const [lamports, spl, t22] = await Promise.all([
26
+ conn.getBalance(owner),
27
+ conn.getParsedTokenAccountsByOwner(owner, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') }),
28
+ conn.getParsedTokenAccountsByOwner(owner, { programId: new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb') }).catch(() => ({ value: [] })),
29
+ ]);
30
+ out[SOL] = lamports / 1e9;
31
+ for (const { account } of [...spl.value, ...t22.value]) {
32
+ const info = account.data.parsed?.info;
33
+ const ui = Number(info?.tokenAmount?.uiAmount ?? 0);
34
+ if (info && ui > 0)
35
+ out[info.mint] = (out[info.mint] ?? 0) + ui;
36
+ }
37
+ return out;
38
+ };
39
+ }
package/dist/server.d.ts CHANGED
@@ -1,14 +1,25 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type FetchFn } from './api.js';
3
3
  import { SOL_MINT, USDC_MINT, type Config } from './config.js';
4
+ import { type BalancesFn } from './trade.js';
4
5
  import { type Signer } from './wallet.js';
5
- export declare const VERSION = "0.1.0";
6
+ import { type TxRpc } from './agentTools.js';
7
+ import type { Rpc } from './transfer.js';
8
+ import type { StrategyStore } from './strategies/store.js';
9
+ import type { Scheduler } from './strategies/scheduler.js';
10
+ export declare const VERSION = "0.3.0";
6
11
  export interface ServerDeps {
7
12
  config?: Config;
8
13
  signer?: Signer | null;
9
14
  fetchFn?: FetchFn;
10
15
  /** Balance reader; defaults to an RPC connection. Injectable for tests. */
11
- balances?: (wallet: string) => Promise<Record<string, number>>;
16
+ balances?: BalancesFn;
17
+ /** Strategy persistence and scheduler (P11); defaults are created from config when omitted. */
18
+ strategyStore?: StrategyStore;
19
+ scheduler?: Scheduler;
20
+ /** Transfer / transaction RPC surfaces; injectable for tests (P13). */
21
+ rpc?: Rpc;
22
+ txRpc?: TxRpc;
12
23
  }
13
24
  export declare function createServer(deps?: ServerDeps): McpServer;
14
25
  export { SOL_MINT, USDC_MINT };
package/dist/server.js CHANGED
@@ -1,16 +1,16 @@
1
1
  // PRD P10 — the Potswap MCP server. Tools are thin, safe wrappers over the Potswap API.
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { Connection, PublicKey, VersionedTransaction } from '@solana/web3.js';
3
+ import { Connection, PublicKey } from '@solana/web3.js';
4
4
  import { z } from 'zod';
5
5
  import { PotswapApi, ApiError } from './api.js';
6
6
  import { loadConfig, NO_WALLET, SOL_MINT, USDC_MINT } from './config.js';
7
- import { checkLimits } from './limits.js';
8
- import { explorer, fromRaw, toRaw } from './format.js';
7
+ import { buildQuote, executeSwap } from './trade.js';
9
8
  import { redact, redactValue } from './redact.js';
10
9
  import { loadSigner } from './wallet.js';
11
- import { describeOrderError, friendlyError } from './shared/errors.js';
12
10
  import { riskBadges, riskLevel } from './shared/risk.js';
13
- export const VERSION = '0.1.0';
11
+ import { registerStrategyTools } from './strategies/tools.js';
12
+ import { registerAgentTools } from './agentTools.js';
13
+ export const VERSION = '0.3.0';
14
14
  const ok = (data, summary) => ({
15
15
  content: [{ type: 'text', text: redact(summary ?? JSON.stringify(data, null, 2)) }], structuredContent: redactValue(data),
16
16
  });
@@ -19,9 +19,10 @@ const fail = (message, extra = {}) => ({
19
19
  });
20
20
  export function createServer(deps = {}) {
21
21
  const config = deps.config ?? loadConfig();
22
- const signer = deps.signer !== undefined ? deps.signer : loadSigner(config);
22
+ const signer = deps.signer !== undefined ? deps.signer : loadSigner({ privateKey: config.privateKey, keypairPath: config.keypairPath, stateDir: config.stateDir });
23
23
  const api = new PotswapApi(config.apiBase, deps.fetchFn);
24
24
  const readBalances = deps.balances ?? rpcBalances(config.rpcUrl);
25
+ const trade = { api, config, signer, balances: readBalances };
25
26
  const server = new McpServer({ name: 'potswap', version: VERSION });
26
27
  const walletOr = (w) => w ?? signer?.publicKey ?? null;
27
28
  server.registerTool('search_tokens', {
@@ -73,7 +74,7 @@ export function createServer(deps = {}) {
73
74
  }, async (a) => {
74
75
  try {
75
76
  const q = await buildQuote(api, a);
76
- return ok(q, `${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol} via ${q.router} (fee ${q.feeBps} bps, impact ${q.priceImpactPct}%)`);
77
+ return ok({ ...q }, `${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol} via ${q.router} (fee ${q.feeLabel ?? `${q.feeBps} bps`}, impact ${q.priceImpactPct}%)`);
77
78
  }
78
79
  catch (e) {
79
80
  return fail(msg(e));
@@ -84,48 +85,14 @@ export function createServer(deps = {}) {
84
85
  description: 'Executes a swap at the best route. Requires confirm=true; otherwise returns the quote only. Protected by POTSWAP_MAX_TRADE_SOL and POTSWAP_MAX_SLIPPAGE_BPS. Every error states whether funds were debited.',
85
86
  inputSchema: { ...quoteShape, confirm: z.boolean().default(false), maxSlippageBps: z.number().int().min(1).max(5000).optional() },
86
87
  }, async (a) => {
87
- if (!signer)
88
- return fail(NO_WALLET);
89
- try {
90
- const q = await buildQuote(api, a);
91
- const slippageBps = a.slippageBps ?? a.maxSlippageBps;
92
- const prices = await api.prices([SOL_MINT, a.inputMint]).catch(() => ({}));
93
- let balanceUi;
94
- try {
95
- balanceUi = (await readBalances(signer.publicKey))[a.inputMint] ?? 0;
96
- }
97
- catch {
98
- balanceUi = undefined;
99
- }
100
- const refusal = checkLimits({
101
- amountUi: a.amount, inputMint: a.inputMint, inputSymbol: q.inputSymbol, isSol: a.inputMint === SOL_MINT,
102
- inputUsd: prices[a.inputMint] ? a.amount * prices[a.inputMint] : undefined, solUsd: prices[SOL_MINT],
103
- slippageBps, maxTradeSol: config.maxTradeSol, maxSlippageBps: config.maxSlippageBps, balanceUi,
104
- });
105
- if (refusal)
106
- return fail(refusal, { quote: q });
107
- if (!a.confirm)
108
- return ok({ confirmRequired: true, quote: q }, `Quote only (confirm=true to execute): ${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol} via ${q.router}`);
109
- if (config.dryRun)
110
- return ok({ dryRun: true, quote: q }, `DRY RUN, nothing signed: ${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol}`);
111
- const order = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: q.inAmountRaw, taker: signer.publicKey, slippageBps });
112
- if (!order.transaction)
113
- return fail(`${describeOrderError(order)} No funds were debited.`, { quote: q });
114
- const tx = VersionedTransaction.deserialize(Buffer.from(order.transaction, 'base64'));
115
- signer.sign(tx);
116
- const res = await api.execute(Buffer.from(tx.serialize()).toString('base64'), order.requestId);
117
- if (res.status !== 'Success')
118
- return fail(friendlyError(res.error || `Swap failed (code ${res.code})`), { signature: res.signature, explorerUrl: res.signature ? explorer(res.signature) : undefined });
119
- const outDec = (await api.token(a.outputMint)).decimals;
120
- const outAmount = fromRaw(res.totalOutputAmount ?? order.outAmount, outDec);
121
- const feeBps = order.platformFee?.feeBps ?? order.feeBps ?? 0;
122
- const feeUsd = prices[a.inputMint] ? a.amount * prices[a.inputMint] * feeBps / 10_000 : undefined;
123
- const cfg = await api.config();
124
- const result = { signature: res.signature, explorerUrl: explorer(res.signature), inAmount: a.amount, inputSymbol: q.inputSymbol, outAmount, outputSymbol: q.outputSymbol, router: order.router, feeBps, feeUsd, pointsAdded: cfg.referralAccount ? feeUsd : 0 };
125
- return ok(result, `Swapped ${a.amount} ${q.inputSymbol} → ${outAmount} ${q.outputSymbol} via ${order.router}. ${explorer(res.signature)}`);
126
- }
127
- catch (e) {
128
- return fail(friendlyError(msg(e)));
88
+ const out = await executeSwap(trade, { inputMint: a.inputMint, outputMint: a.outputMint, amount: a.amount, slippageBps: a.slippageBps ?? a.maxSlippageBps, confirm: a.confirm });
89
+ switch (out.kind) {
90
+ case 'no-wallet': return fail(out.message);
91
+ case 'refused': return fail(out.message, { quote: out.quote });
92
+ case 'quote-only': return ok({ confirmRequired: true, quote: out.quote }, `Quote only (confirm=true to execute): ${a.amount} ${out.quote.inputSymbol} → ${out.quote.outAmount} ${out.quote.outputSymbol} via ${out.quote.router}`);
93
+ case 'dry-run': return ok({ dryRun: true, quote: out.quote }, `DRY RUN, nothing signed: ${a.amount} ${out.quote.inputSymbol} → ${out.quote.outAmount} ${out.quote.outputSymbol}`);
94
+ case 'failed': return fail(out.message, { signature: out.signature, explorerUrl: out.explorerUrl, quote: out.quote });
95
+ case 'ok': return ok({ ...out.result }, `Swapped ${a.amount} ${out.result.inputSymbol} → ${out.result.outAmount} ${out.result.outputSymbol} via ${out.result.router}. ${out.result.explorerUrl}`);
129
96
  }
130
97
  });
131
98
  server.registerTool('get_balances', {
@@ -148,26 +115,6 @@ export function createServer(deps = {}) {
148
115
  return fail(msg(e));
149
116
  }
150
117
  });
151
- server.registerTool('get_positions', {
152
- title: 'Positions',
153
- description: 'Token holdings with 24h price change (PnL arrives in a later version).',
154
- inputSchema: { wallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional() },
155
- }, async ({ wallet }) => {
156
- const w = walletOr(wallet);
157
- if (!w)
158
- return fail(NO_WALLET);
159
- try {
160
- const bal = await readBalances(w);
161
- const mints = Object.keys(bal).filter((m) => m !== SOL_MINT).slice(0, 20);
162
- const infos = mints.length ? await api.searchTokens(mints.join(',')).catch(() => []) : [];
163
- const prices = await api.prices([SOL_MINT, ...mints]).catch(() => ({}));
164
- const positions = mints.map((m) => { const t = infos.find((x) => x.id === m); return { mint: m, symbol: t?.symbol ?? m.slice(0, 4), amount: bal[m], usd: prices[m] ? bal[m] * prices[m] : undefined }; });
165
- return ok({ wallet: w, sol: bal[SOL_MINT] ?? 0, solUsd: prices[SOL_MINT] ? (bal[SOL_MINT] ?? 0) * prices[SOL_MINT] : undefined, positions });
166
- }
167
- catch (e) {
168
- return fail(msg(e));
169
- }
170
- });
171
118
  server.registerTool('get_points', {
172
119
  title: 'POT points',
173
120
  description: "Season 1 points for a wallet: this epoch, all-time, POT allocated and claimable, next unlock. Defaults to the agent wallet.",
@@ -199,6 +146,8 @@ export function createServer(deps = {}) {
199
146
  return fail(msg(e));
200
147
  }
201
148
  });
149
+ registerAgentTools(server, { trade, config, rpc: deps.rpc, txRpc: deps.txRpc, schedulerActive: () => !!deps.scheduler?.active });
150
+ registerStrategyTools(server, { trade, store: deps.strategyStore, scheduler: deps.scheduler });
202
151
  return server;
203
152
  }
204
153
  const quoteShape = {
@@ -207,21 +156,6 @@ const quoteShape = {
207
156
  amount: z.number().positive().describe('Amount in UI units of the input token (1 = 1 SOL)'),
208
157
  slippageBps: z.number().int().min(1).max(5000).optional().describe('Fixed slippage in bps; omit for auto'),
209
158
  };
210
- async function buildQuote(api, a) {
211
- const [inTok, outTok] = await Promise.all([api.token(a.inputMint), api.token(a.outputMint)]);
212
- const inAmountRaw = toRaw(a.amount, inTok.decimals);
213
- const o = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: inAmountRaw, slippageBps: a.slippageBps });
214
- if (o.errorMessage && !o.outAmount)
215
- throw new Error(`${describeOrderError(o)} No funds were debited.`);
216
- const outAmount = fromRaw(o.outAmount, outTok.decimals);
217
- return {
218
- inputMint: a.inputMint, inputSymbol: inTok.symbol, outputMint: a.outputMint, outputSymbol: outTok.symbol,
219
- inAmount: a.amount, inAmountRaw, outAmount, price: outAmount / a.amount,
220
- priceImpactPct: o.priceImpactPct !== undefined ? Math.abs(Number(o.priceImpactPct)) : undefined,
221
- slippageBps: o.slippageBps, feeBps: o.platformFee?.feeBps ?? o.feeBps ?? 0, router: o.router, requestId: o.requestId,
222
- expiresAt: o.expireAt ?? null,
223
- };
224
- }
225
159
  function slimToken(t) {
226
160
  return { mint: t.id, symbol: t.symbol, name: t.name, decimals: t.decimals, usdPrice: t.usdPrice, mcap: t.mcap, liquidity: t.liquidity, isVerified: t.isVerified ?? false };
227
161
  }
@@ -0,0 +1,24 @@
1
+ export interface FeeSchedule {
2
+ standardBps: number;
3
+ sizeBps: number;
4
+ sizeThresholdUsd: number;
5
+ stakerDiscountBps: number;
6
+ /** POT that must be staked for the discount; null until TGE (discount disabled). */
7
+ stakerMinPot: number | null;
8
+ }
9
+ export declare const JUP_MIN_BPS = 50;
10
+ export declare const JUP_MAX_BPS = 255;
11
+ export declare const DEFAULT_FEE_SCHEDULE: FeeSchedule;
12
+ export type FeeTier = 'standard' | 'size' | 'staker';
13
+ export interface FeeDecision {
14
+ bps: number;
15
+ tier: FeeTier;
16
+ label: string;
17
+ }
18
+ /**
19
+ * @param inputUsd USD value of the input amount at quote time; undefined when no price is known (→ standard tier)
20
+ * @param stakedPot POT staked by the wallet; 0 / undefined when unknown
21
+ */
22
+ export declare function feeBpsFor(schedule: FeeSchedule, inputUsd?: number, stakedPot?: number): FeeDecision;
23
+ /** Parses the schedule from /api/config, falling back to defaults field by field. */
24
+ export declare function parseFeeSchedule(raw: unknown): FeeSchedule;
@@ -0,0 +1,42 @@
1
+ // GENERATED by scripts/sync-shared.mjs from app/src/lib/fee.ts. Do not edit here.
2
+ // PRD P8.12–P8.15 — the fee schedule. One pure function shared by the web app and the MCP
3
+ // (mcp/src/shared/fee.ts is a generated copy). Jupiter allows 50–255 bps.
4
+ export const JUP_MIN_BPS = 50;
5
+ export const JUP_MAX_BPS = 255;
6
+ export const DEFAULT_FEE_SCHEDULE = {
7
+ standardBps: 250,
8
+ sizeBps: 100,
9
+ sizeThresholdUsd: 1000,
10
+ stakerDiscountBps: 100,
11
+ stakerMinPot: null,
12
+ };
13
+ const clamp = (bps) => Math.min(JUP_MAX_BPS, Math.max(JUP_MIN_BPS, Math.round(bps)));
14
+ /**
15
+ * @param inputUsd USD value of the input amount at quote time; undefined when no price is known (→ standard tier)
16
+ * @param stakedPot POT staked by the wallet; 0 / undefined when unknown
17
+ */
18
+ export function feeBpsFor(schedule, inputUsd, stakedPot = 0) {
19
+ const size = inputUsd !== undefined && inputUsd >= schedule.sizeThresholdUsd;
20
+ let bps = size ? schedule.sizeBps : schedule.standardBps;
21
+ let tier = size ? 'size' : 'standard';
22
+ if (schedule.stakerMinPot !== null && stakedPot >= schedule.stakerMinPot) {
23
+ bps -= schedule.stakerDiscountBps;
24
+ tier = 'staker';
25
+ }
26
+ bps = clamp(bps);
27
+ const pct = `${bps / 100}%`;
28
+ const label = tier === 'standard' ? pct : tier === 'size' ? `${pct} (size tier)` : `${pct} (staker)`;
29
+ return { bps, tier, label };
30
+ }
31
+ /** Parses the schedule from /api/config, falling back to defaults field by field. */
32
+ export function parseFeeSchedule(raw) {
33
+ const r = (raw ?? {});
34
+ const num = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
35
+ return {
36
+ standardBps: clamp(num(r.standardBps, DEFAULT_FEE_SCHEDULE.standardBps)),
37
+ sizeBps: clamp(num(r.sizeBps, DEFAULT_FEE_SCHEDULE.sizeBps)),
38
+ sizeThresholdUsd: num(r.sizeThresholdUsd, DEFAULT_FEE_SCHEDULE.sizeThresholdUsd),
39
+ stakerDiscountBps: num(r.stakerDiscountBps, DEFAULT_FEE_SCHEDULE.stakerDiscountBps),
40
+ stakerMinPot: typeof r.stakerMinPot === 'number' && r.stakerMinPot > 0 ? r.stakerMinPot : null,
41
+ };
42
+ }
@@ -0,0 +1,43 @@
1
+ import type { DcaStrategy, LimitStrategy, RebalanceStrategy, Run, Strategy, TpSlStrategy } from './types.js';
2
+ export declare const SOL_MINT = "So11111111111111111111111111111111111111112";
3
+ /** True when a time-based strategy should run at `now`. */
4
+ export declare function isDue(s: Strategy, now: number): boolean;
5
+ /**
6
+ * Next run after a run at `now`. A missed window runs once; the next slot is
7
+ * always in the future (P11.2), never a catch-up burst.
8
+ */
9
+ export declare function nextRunAfter(s: DcaStrategy | RebalanceStrategy, now: number): string;
10
+ export declare function dcaIsComplete(s: DcaStrategy): boolean;
11
+ export interface Leg {
12
+ inputMint: string;
13
+ outputMint: string;
14
+ amount: number;
15
+ reason: string;
16
+ }
17
+ /**
18
+ * Rebalance plan (P11.4): weights from balances × prices; assets drifting more than
19
+ * `driftPct` points are brought back via SOL. Returns [] when nothing drifts.
20
+ * Unpriced targets are excluded and reported in `warnings`.
21
+ */
22
+ export declare function rebalancePlan(s: RebalanceStrategy, balances: Record<string, number>, prices: Record<string, number>): {
23
+ legs: Leg[];
24
+ warnings: string[];
25
+ weights: Record<string, number>;
26
+ };
27
+ export type TpSlTrigger = {
28
+ trigger: 'take-profit' | 'stop-loss' | 'trailing-stop';
29
+ threshold: number;
30
+ } | null;
31
+ /** TP/SL decision (P11.5). `price` undefined = no decision. Returns the updated high-water mark too. */
32
+ export declare function tpslDecision(s: TpSlStrategy, price: number | undefined): {
33
+ trigger: TpSlTrigger;
34
+ highWater: number;
35
+ };
36
+ /** SOL-equivalent spent by all strategies in the UTC day containing `now` (P11.6). */
37
+ export declare function solSpentToday(runs: Run[], now: number): number;
38
+ export declare function dcaLegFor(s: DcaStrategy): Leg;
39
+ /** Limit order decision (P13.10): 'fill' when the condition holds, 'expired' past expiresAt, else null. */
40
+ export declare function limitDecision(s: LimitStrategy, price: number | undefined, now: number): 'fill' | 'expired' | null;
41
+ export declare function limitLegFor(s: LimitStrategy): Leg;
42
+ export declare const MAX_CONSECUTIVE_FAILURES = 3;
43
+ export declare function newId(prefix: string, now?: number): string;