@pnlmarket/mcp-server 0.4.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +83 -0
  5. package/dist/install.d.ts +1 -0
  6. package/dist/install.js +168 -0
  7. package/dist/lib/output.d.ts +95 -0
  8. package/dist/lib/output.js +175 -0
  9. package/dist/lib/passphrase.d.ts +16 -0
  10. package/dist/lib/passphrase.js +57 -0
  11. package/dist/lib/pnl-api.d.ts +65 -0
  12. package/dist/lib/pnl-api.js +89 -0
  13. package/dist/lib/sign.d.ts +40 -0
  14. package/dist/lib/sign.js +126 -0
  15. package/dist/lib/wallet.d.ts +74 -0
  16. package/dist/lib/wallet.js +405 -0
  17. package/dist/tools/browse-markets.d.ts +12 -0
  18. package/dist/tools/browse-markets.js +91 -0
  19. package/dist/tools/claim-now.d.ts +10 -0
  20. package/dist/tools/claim-now.js +113 -0
  21. package/dist/tools/claim.d.ts +10 -0
  22. package/dist/tools/claim.js +43 -0
  23. package/dist/tools/export-keypair.d.ts +10 -0
  24. package/dist/tools/export-keypair.js +25 -0
  25. package/dist/tools/get-market.d.ts +10 -0
  26. package/dist/tools/get-market.js +58 -0
  27. package/dist/tools/help.d.ts +7 -0
  28. package/dist/tools/help.js +54 -0
  29. package/dist/tools/init.d.ts +7 -0
  30. package/dist/tools/init.js +69 -0
  31. package/dist/tools/notify.d.ts +12 -0
  32. package/dist/tools/notify.js +150 -0
  33. package/dist/tools/pitch-idea.d.ts +38 -0
  34. package/dist/tools/pitch-idea.js +176 -0
  35. package/dist/tools/pitch-now.d.ts +39 -0
  36. package/dist/tools/pitch-now.js +179 -0
  37. package/dist/tools/restore.d.ts +11 -0
  38. package/dist/tools/restore.js +45 -0
  39. package/dist/tools/set-username.d.ts +10 -0
  40. package/dist/tools/set-username.js +87 -0
  41. package/dist/tools/unlock.d.ts +17 -0
  42. package/dist/tools/unlock.js +47 -0
  43. package/dist/tools/vote-now.d.ts +13 -0
  44. package/dist/tools/vote-now.js +146 -0
  45. package/dist/tools/vote.d.ts +12 -0
  46. package/dist/tools/vote.js +49 -0
  47. package/dist/tools/wallet.d.ts +7 -0
  48. package/dist/tools/wallet.js +40 -0
  49. package/package.json +64 -0
  50. package/skills/README.md +45 -0
  51. package/skills/pnl-browse/SKILL.md +30 -0
  52. package/skills/pnl-claim/SKILL.md +60 -0
  53. package/skills/pnl-claim-now/SKILL.md +67 -0
  54. package/skills/pnl-export/SKILL.md +39 -0
  55. package/skills/pnl-help/SKILL.md +17 -0
  56. package/skills/pnl-init/SKILL.md +24 -0
  57. package/skills/pnl-lock/SKILL.md +17 -0
  58. package/skills/pnl-name/SKILL.md +32 -0
  59. package/skills/pnl-notify/SKILL.md +57 -0
  60. package/skills/pnl-pitch/SKILL.md +83 -0
  61. package/skills/pnl-pitch-now/SKILL.md +88 -0
  62. package/skills/pnl-restore/SKILL.md +38 -0
  63. package/skills/pnl-unlock/SKILL.md +28 -0
  64. package/skills/pnl-vote/SKILL.md +48 -0
  65. package/skills/pnl-vote-now/SKILL.md +68 -0
  66. package/skills/pnl-wallet/SKILL.md +22 -0
@@ -0,0 +1,146 @@
1
+ import { z } from 'zod';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ import { requireUnlockedKeypair, loadConfig, getConnection, getBalanceSol, } from '../lib/wallet.js';
4
+ import { signSerializedTx, sendAndConfirm, freshNonce, signChallenge, challenge, signedRequestHash, } from '../lib/sign.js';
5
+ import { getMarket } from '../lib/pnl-api.js';
6
+ import { Badge, headline, code, kvTable, inline, next, reply, hr } from '../lib/output.js';
7
+ // ─── pnl_vote_now ────────────────────────────────────────────────
8
+ //
9
+ // Autosign buy_yes / buy_no. Same shape as pnl_pitch_now: build the
10
+ // unsigned tx server-side, sign + send locally, sig-auth complete-vote
11
+ // to write trade history + bump participant counts.
12
+ export const voteNowInputSchema = {
13
+ marketId: z
14
+ .string()
15
+ .min(1)
16
+ .describe('Market id from pnl_browse_markets or pnl_get_market. Accepts either the Mongo id or the on-chain market address — we resolve to the on-chain address internally.'),
17
+ vote: z
18
+ .enum(['yes', 'no'])
19
+ .describe("'yes' to back the idea, 'no' to fade it. NO voters split the pool if NO wins."),
20
+ amountSol: z
21
+ .number()
22
+ .positive()
23
+ .describe('Stake in SOL. Minimum 0.01 SOL. Total cost is amount + ~0.000005 SOL Solana tx fee.'),
24
+ autosignCapSol: z
25
+ .number()
26
+ .positive()
27
+ .optional()
28
+ .describe('Optional cap override that can only LOWER the autosign limit for this call, never raise it. Ceiling is from ~/.config/pnl/config.json (default 0.05 SOL). Stakes above the (lowered) cap fail before any signing — use pnl_vote for the deep-link flow in that case.'),
29
+ };
30
+ const VoteNowInput = z.object(voteNowInputSchema);
31
+ function getApiBase() {
32
+ const raw = process.env.PNL_API_BASE_URL?.trim();
33
+ if (!raw)
34
+ return 'https://pnl.market';
35
+ return raw.endsWith('/') ? raw.slice(0, -1) : raw;
36
+ }
37
+ export async function callVoteNow(rawInput) {
38
+ const input = VoteNowInput.parse(rawInput ?? {});
39
+ // Cap check before any I/O. The user's stake itself is the dominant
40
+ // cost — Solana tx fees are ~5000 lamports (0.000005 SOL).
41
+ //
42
+ // Cap policy: the user's configured cap is the ceiling — the
43
+ // per-call autosignCapSol arg can only LOWER it, never raise.
44
+ // To raise the cap the user edits ~/.config/pnl/config.json directly.
45
+ const configCap = loadConfig().autosignCapSol;
46
+ const cap = input.autosignCapSol != null
47
+ ? Math.min(input.autosignCapSol, configCap)
48
+ : configCap;
49
+ if (input.amountSol > cap) {
50
+ throw new Error(`Stake ${input.amountSol} SOL exceeds autosign cap ${cap} SOL. Either raise the cap (autosignCapSol arg) or use pnl_vote for the browser deep-link flow.`);
51
+ }
52
+ const keypair = requireUnlockedKeypair();
53
+ const walletAddress = keypair.publicKey.toBase58();
54
+ const base = getApiBase();
55
+ // Resolve marketId → on-chain address. The user-facing arg accepts
56
+ // either the Mongo id (returned by pnl_browse_markets) or the
57
+ // already-base58 market address; the on-chain build needs the
58
+ // latter. /api/markets/<id> returns both so we can be flexible.
59
+ let marketAddress;
60
+ let onchainId;
61
+ try {
62
+ new PublicKey(input.marketId);
63
+ marketAddress = input.marketId;
64
+ onchainId = input.marketId; // assume the user supplied the on-chain one
65
+ // For the complete-vote call we still want the Mongo id (it's what
66
+ // /market/<id> uses + how participants are keyed). Look it up.
67
+ const market = await getMarket(input.marketId);
68
+ onchainId = market.id ?? input.marketId;
69
+ }
70
+ catch {
71
+ // Not a valid base58 pubkey — treat as Mongo id.
72
+ const market = await getMarket(input.marketId);
73
+ if (!market.marketAddress) {
74
+ throw new Error(`market ${input.marketId} has no marketAddress — cannot autosign vote`);
75
+ }
76
+ marketAddress = market.marketAddress;
77
+ onchainId = market.id ?? input.marketId;
78
+ }
79
+ // Balance sanity-check.
80
+ const balance = await getBalanceSol(new PublicKey(walletAddress));
81
+ const required = input.amountSol + 0.001; // stake + comfortable tx-fee buffer
82
+ if (balance < required) {
83
+ throw new Error(`Wallet balance ${balance.toFixed(4)} SOL is below the ${required.toFixed(4)} SOL needed (stake + fee buffer). Fund ${walletAddress} and try again.`);
84
+ }
85
+ // 1. build-vote-tx
86
+ const buildRes = await fetch(`${base}/api/mcp/markets/build-vote-tx`, {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
89
+ body: JSON.stringify({
90
+ walletAddress,
91
+ marketAddress,
92
+ voteType: input.vote,
93
+ amountSol: input.amountSol,
94
+ }),
95
+ });
96
+ const buildJson = (await buildRes.json());
97
+ if (!buildRes.ok || !buildJson.success || !buildJson.data) {
98
+ throw new Error(`build-vote-tx failed (${buildRes.status}): ${buildJson.error || 'unknown error'}`);
99
+ }
100
+ const built = buildJson.data;
101
+ // 2. sign + send locally
102
+ const rawTx = signSerializedTx(built.tx, keypair);
103
+ const { signature: txSignature } = await sendAndConfirm(rawTx, getConnection(), {
104
+ confirmTimeoutMs: 90_000,
105
+ });
106
+ // 3. Build complete-vote body, hash it into the challenge, sign.
107
+ // The hash binds the sig to voteType + amountSol + marketId so an
108
+ // attacker who captures the sig within the nonce window cannot
109
+ // rewrite the side or amount on the persisted trade row.
110
+ const nonce = freshNonce();
111
+ const completeBodyCore = {
112
+ txSignature,
113
+ marketId: onchainId,
114
+ voteType: input.vote,
115
+ amountSol: input.amountSol,
116
+ };
117
+ const payloadHash = signedRequestHash(completeBodyCore);
118
+ const sig = signChallenge(challenge('complete-vote', txSignature, nonce, payloadHash), keypair);
119
+ const completeRes = await fetch(`${base}/api/mcp/markets/complete-vote`, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
122
+ body: JSON.stringify({
123
+ walletAddress,
124
+ nonce,
125
+ signature: sig,
126
+ ...completeBodyCore,
127
+ }),
128
+ });
129
+ const completeJson = (await completeRes.json());
130
+ if (!completeRes.ok || !completeJson.success || !completeJson.data) {
131
+ throw new Error(`Tx confirmed on-chain (${txSignature}) but complete-vote failed (${completeRes.status}): ${completeJson.error || 'unknown error'}. The on-chain vote is recorded — re-running pnl_vote_now with the same args is idempotent on the tx signature.`);
132
+ }
133
+ const done = completeJson.data;
134
+ const side = input.vote === 'yes' ? 'YES' : 'NO';
135
+ const sideHint = input.vote === 'yes'
136
+ ? '(backing the idea)'
137
+ : "(fading the idea — split the pool with other critics if NO wins)";
138
+ return reply(headline(`${Badge.live} ${side} ${input.amountSol} SOL recorded`), kvTable([
139
+ ['Market', `${base}/market/${onchainId}`],
140
+ ['Side', `${side} ${sideHint}`],
141
+ ['Stake', `${input.amountSol} SOL`],
142
+ ['Wallet', inline(walletAddress)],
143
+ ['Tx', `[${txSignature.slice(0, 8)}…${txSignature.slice(-6)}](${done.solscan})`],
144
+ done.alreadyExists ? ['Note', 'idempotent retry — vote was already recorded'] : null,
145
+ ].filter((r) => Array.isArray(r))), hr, `The MCP signed and sent the \`buy_${input.vote}\` transaction locally — no browser bounce needed because the stake (${input.amountSol} SOL) was within the autosign cap (${cap} SOL).`, code(`Tx: ${txSignature}`), next(`Check pool movement at ${inline(`${base}/market/${onchainId}`)}.`));
146
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ export declare const voteInputSchema: {
3
+ readonly marketId: z.ZodString;
4
+ readonly vote: z.ZodEnum<["yes", "no"]>;
5
+ readonly amountSol: z.ZodNumber;
6
+ };
7
+ export declare function callVote(rawInput: unknown): Promise<{
8
+ content: Array<{
9
+ type: "text";
10
+ text: string;
11
+ }>;
12
+ }>;
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import { Badge, headline, code, kvTable, next, reply } from '../lib/output.js';
3
+ // ─── pnl_vote ────────────────────────────────────────────────────
4
+ //
5
+ // Prepare a YES/NO stake on an existing market and return a deep-link
6
+ // URL with ?vote=<side>&amount=<sol> query params. The market detail
7
+ // page reads those on mount and pre-fills the vote panel — the user
8
+ // confirms + signs in their browser wallet.
9
+ //
10
+ // Like pnl_pitch_idea, v0.2 is deep-link only. Phase B autosign will
11
+ // build + sign the buy_yes / buy_no transaction locally for stakes
12
+ // under the autosign cap.
13
+ export const voteInputSchema = {
14
+ marketId: z
15
+ .string()
16
+ .min(1)
17
+ .describe("The market id from pnl_browse_markets or pnl_get_market. Either the Mongo document id or the on-chain market address works."),
18
+ vote: z
19
+ .enum(['yes', 'no'])
20
+ .describe("'yes' to back the idea (you think it deserves to launch). 'no' to fade it (you think it won't / shouldn't). NO voters split the pool if NO wins — they get paid for filtering noise."),
21
+ amountSol: z
22
+ .number()
23
+ .positive()
24
+ .describe("How much SOL to stake. Minimum 0.01 SOL on most markets. Typical range 0.01-1 SOL for retail-sized votes."),
25
+ };
26
+ const VoteInput = z.object(voteInputSchema);
27
+ function getApiBase() {
28
+ const raw = process.env.PNL_API_BASE_URL?.trim();
29
+ if (!raw)
30
+ return 'https://pnl.market';
31
+ return raw.endsWith('/') ? raw.slice(0, -1) : raw;
32
+ }
33
+ export async function callVote(rawInput) {
34
+ const { marketId, vote, amountSol } = VoteInput.parse(rawInput ?? {});
35
+ // We trust the market id format and let the live page validate
36
+ // existence — no point hitting /api/markets/<id> here just to fail
37
+ // before the user opens the link.
38
+ const base = getApiBase();
39
+ const url = `${base}/market/${encodeURIComponent(marketId)}?vote=${vote}&amount=${amountSol}`;
40
+ const side = vote === 'yes' ? 'YES' : 'NO';
41
+ const sideHint = vote === 'yes'
42
+ ? "(backing the idea — you think it deserves to launch)"
43
+ : "(fading the idea — you'll split the pool with other critics if NO wins)";
44
+ return reply(headline(`${Badge.draft} ${side} ${amountSol} SOL on \`${marketId}\``), `Open this URL to confirm and sign in your browser wallet:`, code(url), kvTable([
45
+ ['Side', `${side} ${sideHint}`],
46
+ ['Amount', `${amountSol} SOL`],
47
+ ['Market', `\`${marketId}\``],
48
+ ]), `The market detail page pre-fills the vote panel from the URL. You'll confirm the side + amount and sign the \`buy_yes\` (or \`buy_no\`) transaction with your wallet (Privy session, or external Phantom if you imported the keypair via \`pnl_export_keypair\`). Transaction confirms in ~5-15s on Solana mainnet.`, next('Open the URL in a browser to sign.'));
49
+ }
@@ -0,0 +1,7 @@
1
+ export declare const walletInputSchema: {};
2
+ export declare function callWallet(_rawInput: unknown): Promise<{
3
+ content: Array<{
4
+ type: "text";
5
+ text: string;
6
+ }>;
7
+ }>;
@@ -0,0 +1,40 @@
1
+ import { getAddress, getBalanceSol, hasWallet, loadConfig, getRpcUrl, isUsingHostedRpc, unlockStatus, } from '../lib/wallet.js';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ import { Badge, headline, kvTable, inline, truncAddress, next, reply } from '../lib/output.js';
4
+ // ─── pnl_wallet ──────────────────────────────────────────────────
5
+ //
6
+ // Read-only status snapshot. Address (truncated + full), balance,
7
+ // lock state, autosign cap, RPC. Doesn't require unlock.
8
+ export const walletInputSchema = {};
9
+ export async function callWallet(_rawInput) {
10
+ if (!hasWallet()) {
11
+ return reply(headline('No PNL wallet on this machine yet.'), `Run ${inline('pnl_init')} to generate one — fresh BIP39 mnemonic, encrypted local keypair, deposit address ready in seconds.`, next(`Type \`/pnl-init\` or ask the agent to set up PNL.`));
12
+ }
13
+ const address = getAddress();
14
+ const config = loadConfig();
15
+ const { unlocked, secondsRemaining } = unlockStatus();
16
+ let balance;
17
+ try {
18
+ const sol = await getBalanceSol(new PublicKey(address));
19
+ balance = `${sol.toFixed(4)} SOL`;
20
+ }
21
+ catch (e) {
22
+ balance = `(lookup failed — ${e instanceof Error ? e.message.slice(0, 60) : 'network'})`;
23
+ }
24
+ const lockState = unlocked
25
+ ? `${Badge.unlocked} ${Math.floor(secondsRemaining / 60)}m ${secondsRemaining % 60}s remaining`
26
+ : `${Badge.locked} — run \`/pnl-unlock\` before signing`;
27
+ const hosted = isUsingHostedRpc();
28
+ const rpcLabel = hosted ? `\`${getRpcUrl()}\` (hosted)` : `\`${getRpcUrl()}\``;
29
+ return reply(headline(`${truncAddress(address)} · ${balance} · ${unlocked ? Badge.unlocked : Badge.locked}`), kvTable([
30
+ ['Address', `\`${address}\``],
31
+ ['Balance', balance],
32
+ ['Status', lockState],
33
+ ['Autosign cap', `${config.autosignCapSol} SOL`],
34
+ ['RPC', rpcLabel],
35
+ ]), hosted
36
+ ? `_RPC: pnl.market (hosted) — heavy use? Grab a free Helius key at helius.dev and set ${inline('PNL_RPC_URL')} in your Claude Code mcp config to skip the shared rate limit._`
37
+ : null, unlocked
38
+ ? next('Pitch an idea with `/pnl-pitch` or vote with `/pnl-vote`.')
39
+ : next('Run `/pnl-unlock` to enable signing.'));
40
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@pnlmarket/mcp-server",
3
+ "version": "0.4.0",
4
+ "description": "Model Context Protocol server for PNL — let agents (Claude Code, Cursor, Cline, Codex) browse live conviction markets, pitch ideas as markets, and stake YES/NO without leaving the terminal. Local encrypted wallet, autosign for sub-cap amounts, deep-link for larger ones.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "pnl-mcp-server": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "skills",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx watch src/index.ts",
18
+ "start": "node dist/index.js",
19
+ "prepublishOnly": "pnpm build"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "pnl",
25
+ "solana",
26
+ "prediction-market",
27
+ "conviction-market",
28
+ "launchpad",
29
+ "claude-code",
30
+ "cursor",
31
+ "agent"
32
+ ],
33
+ "author": "Bishwanath Bastola",
34
+ "license": "MIT",
35
+ "homepage": "https://docs.pnl.market/docs/build/mcp-server",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/aitankfish/pnl.git",
39
+ "directory": "apps/mcp"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/aitankfish/pnl/issues"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.18"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.0.0",
52
+ "@solana/web3.js": "^1.95.0",
53
+ "bip39": "^3.1.0",
54
+ "bs58": "^6.0.0",
55
+ "ed25519-hd-key": "^1.3.0",
56
+ "tweetnacl": "^1.0.3",
57
+ "zod": "^3.23.8"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^20.14.0",
61
+ "tsx": "^4.19.0",
62
+ "typescript": "^5.6.0"
63
+ }
64
+ }
@@ -0,0 +1,45 @@
1
+ # PNL skills for Claude Code
2
+
3
+ Short slash commands that wrap the `@pnlmarket/mcp-server` tools. Install
4
+ these once and you can type `/pnl-init`, `/pnl-wallet`, `/pnl-browse`,
5
+ `/pnl-pitch`, `/pnl-export` in any Claude Code session instead of
6
+ asking the agent to call MCP tools by name.
7
+
8
+ ## What they do
9
+
10
+ | Command | What it does | MCP tool it wraps |
11
+ |---|---|---|
12
+ | `/pnl-init` | First-run setup. Generate a local Solana keypair and show the deposit address. | `pnl_init` |
13
+ | `/pnl-wallet` | Show the local wallet's address, balance, autosign cap, RPC. | `pnl_wallet` |
14
+ | `/pnl-browse` | List live conviction markets on PNL with YES%, pool, votes. | `pnl_browse_markets` |
15
+ | `/pnl-pitch` | Pitch an idea from the current conversation. Extracts context, asks the user for missing fields, returns a deep-link to post the market. | `pnl_pitch_idea` |
16
+ | `/pnl-export` | Back up the local keypair (base58 + JSON-array). Requires confirmation. | `pnl_export_keypair` |
17
+
18
+ ## Install
19
+
20
+ Copy each skill directory into your Claude Code skills folder:
21
+
22
+ ```bash
23
+ # macOS / Linux
24
+ cp -R apps/mcp/skills/pnl-* ~/.claude/skills/
25
+
26
+ # or, symlink them so updates flow without recopying:
27
+ for d in apps/mcp/skills/pnl-*; do
28
+ ln -s "$(pwd)/$d" "$HOME/.claude/skills/$(basename "$d")"
29
+ done
30
+ ```
31
+
32
+ Restart Claude Code. The five commands appear in `/help`.
33
+
34
+ ## Prereq
35
+
36
+ You also need the MCP server itself wired into Claude Code's
37
+ `mcpServers` config. See `apps/mcp/README.md` for that — once the
38
+ server is registered, these skills tell the agent how to use it.
39
+
40
+ ## Customization
41
+
42
+ Each skill is a single `SKILL.md` with frontmatter (name +
43
+ description) and free-form instructions to the agent. Edit them to
44
+ match your team's pitch style, default target pool, default duration,
45
+ etc. — they're just markdown.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: pnl-browse
3
+ description: List live conviction markets on PNL. Use to see what ideas are currently open for voting.
4
+ ---
5
+
6
+ # /pnl-browse
7
+
8
+ The user wants to see what's currently live on PNL — names, YES%,
9
+ pool sizes, vote counts.
10
+
11
+ ## What to do
12
+
13
+ 1. Call the `pnl_browse_markets` MCP tool. Defaults are fine — `status: "active"`,
14
+ `limit: 10`. If the user said "show me resolved markets" or "what won
15
+ recently", pass `status: "yesWins"` or `status: "noWins"`.
16
+ 2. Display the formatted summary returned by the tool. Each market line
17
+ shows: name + $ticker + founder, status + YES% + pool + vote count
18
+ + time-left, and a URL the user can click to see the full market.
19
+ 3. If the user wants details on a specific market, suggest they run
20
+ `/pnl-get <market-id>` (the id is in the URL after `/market/`) or
21
+ call `pnl_get_market` directly.
22
+ 4. Don't repeat the raw JSON dump back to the user unless they ask —
23
+ it's there for the agent's reasoning, not display.
24
+
25
+ ## Common follow-ups
26
+
27
+ - "Show me the AI ones" → call again with default args, then filter the
28
+ results by category in your response.
29
+ - "Just YES%" → re-render with only that field per market.
30
+ - "More" → call again with `page: 2`.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: pnl-claim
3
+ description: Claim rewards on a resolved PNL market in deep-link mode. Returns a URL the user opens to sign the claim_rewards tx in their browser wallet.
4
+ ---
5
+
6
+ # /pnl-claim
7
+
8
+ The user has an unclaimed position on a resolved PNL market and
9
+ wants to collect their rewards via the browser flow (no MCP unlock
10
+ needed).
11
+
12
+ ## What to do
13
+
14
+ ### Step 1 — figure out which market
15
+
16
+ If the user named one or said "claim my market", run
17
+ `pnl_browse_markets` (filter by `status: 'yesWins'`, `'noWins'`, or
18
+ `'refund'`) — or use `pnl_notify` to surface "claim_ready"
19
+ notifications if any are pending.
20
+
21
+ If the user is talking about their own positions, the
22
+ `/profile/<wallet>` page is the source of truth for what's
23
+ claimable; suggest opening it if disambiguating between several.
24
+
25
+ ### Step 2 — confirm
26
+
27
+ Quickly check that:
28
+ - The market is resolved (not "active" / "Unresolved")
29
+ - The user expects to be a winner (they had a position on the
30
+ winning side, or it's a refund)
31
+
32
+ If anything is off, redirect to `pnl_browse_markets` or the profile
33
+ page rather than guessing.
34
+
35
+ ### Step 3 — call pnl_claim
36
+
37
+ Pass `{ marketId }`. The tool returns a URL of the form
38
+ `/market/<id>?claim=1`.
39
+
40
+ ### Step 4 — hand off
41
+
42
+ Tell the user to open the URL — the market detail page detects
43
+ `?claim=1`, opens the claim panel, and they sign `claim_rewards`
44
+ in their browser wallet. Tx confirms in ~5-15s.
45
+
46
+ ## Failure modes
47
+
48
+ - **Market not resolved**: backend returns 400. Surface as "this
49
+ market hasn't resolved yet — check back when YES, NO, or refund
50
+ is decided."
51
+ - **No position**: backend returns 404. Surface as "this wallet has
52
+ nothing to claim on that market."
53
+ - **Already claimed**: backend returns 400 with "Rewards already
54
+ claimed for this position".
55
+
56
+ ## When to use `/pnl-claim-now` instead
57
+
58
+ If the user says "claim it for me" or "auto-claim", switch to
59
+ `/pnl-claim-now` — it signs locally without a browser bounce.
60
+ Requires the wallet to be unlocked.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: pnl-claim-now
3
+ description: Autosign claim_rewards — the MCP signs and sends locally. No browser bounce, no autosign cap (claim is a withdrawal, not a spend).
4
+ ---
5
+
6
+ # /pnl-claim-now
7
+
8
+ The user wants to collect rewards on a resolved PNL market without
9
+ leaving the terminal. Unlike `/pnl-pitch-now` and `/pnl-vote-now`
10
+ there is no autosign cap — claim is a withdrawal of funds the user
11
+ is already owed by the program, so capping it would gate the user
12
+ from their own money.
13
+
14
+ ## Prerequisites
15
+
16
+ - Wallet on this machine (`pnl_init` previously)
17
+ - Wallet **unlocked** (`pnl_unlock` first)
18
+ - Market resolved (YES wins, NO wins, or refund) — not Active
19
+ - Wallet has an unclaimed position on the market
20
+
21
+ ## What to do
22
+
23
+ ### Step 1 — figure out which market
24
+
25
+ `pnl_browse_markets` filtered by status, or `pnl_notify` if there
26
+ are claim-ready notifications, or a marketId the user named
27
+ directly.
28
+
29
+ ### Step 2 — confirm autosign
30
+
31
+ In one sentence:
32
+
33
+ > "I'll sign the `claim_rewards` tx locally with your wallet.
34
+ > Confirm?"
35
+
36
+ If the user wants the browser path, switch to `/pnl-claim`.
37
+
38
+ ### Step 3 — call pnl_claim_now
39
+
40
+ Pass `{ marketId }`. The tool:
41
+ 1. Fetches market resolution + position from chain
42
+ 2. Builds the unsigned tx (including Token2022 ATA creation for
43
+ YES wins)
44
+ 3. Signs locally
45
+ 4. Sends + confirms
46
+ 5. Persists via sig-auth complete-claim
47
+
48
+ Returns tx signature + Solscan link + profile URL.
49
+
50
+ ### Step 4 — hand off
51
+
52
+ Tell the user where to see the updated position:
53
+ the `/profile/<wallet>` URL the tool returned. For YES wins, the
54
+ tokens land in the wallet's Token2022 ATA (Phantom / Solflare /
55
+ Backpack will surface them automatically).
56
+
57
+ ## Failure modes
58
+
59
+ - **Wallet locked** → suggest `/pnl-unlock` first.
60
+ - **Market not resolved** → "this market hasn't resolved yet."
61
+ - **No position** → "this wallet has nothing to claim on that
62
+ market."
63
+ - **Already claimed** → no-op; suggest checking the profile to see
64
+ the existing claim.
65
+ - **Tx confirmed but complete-claim failed** → the on-chain claim
66
+ succeeded; the off-chain "claimed" flag failed to update.
67
+ Re-running `pnl_claim_now` is safe (idempotent on tx signature).
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: pnl-export
3
+ description: Export the local PNL keypair (base58 + JSON-array) so the user can back it up or import it into Phantom/Solflare/Backpack. Requires explicit confirmation.
4
+ ---
5
+
6
+ # /pnl-export
7
+
8
+ The user wants to back up the seed of their local PNL wallet, or move
9
+ it into a different Solana wallet (Phantom / Solflare / Backpack /
10
+ Solana CLI).
11
+
12
+ ## What to do
13
+
14
+ 1. **Confirm before exporting.** The keypair is the entire access
15
+ control for the wallet — anyone who reads it can spend the SOL on
16
+ it. Ask the user something like:
17
+
18
+ > "I'm about to show your PNL secret key in this conversation. It
19
+ > can be used by anyone who sees it to spend your funds. Continue?"
20
+
21
+ Only proceed if they say yes.
22
+
23
+ 2. Call the `pnl_export_keypair` MCP tool with `confirm: "EXPORT"`.
24
+
25
+ 3. Display the response. It contains both formats:
26
+ - **base58 string** — paste into Phantom's "Import Private Key"
27
+ (also works for Solflare and Backpack)
28
+ - **64-byte JSON array** — for the Solana CLI
29
+ (`solana config set --keypair /path/to/keypair.json`)
30
+
31
+ 4. Suggest the user:
32
+ - Save the base58 string in a password manager (1Password, Bitwarden, etc.)
33
+ - Clear their terminal scrollback after copying, if they're on a
34
+ shared machine
35
+ - Test the restore in a wallet they control before relying on it
36
+
37
+ 5. If the user pastes the secret somewhere visible (a screenshot, a
38
+ shared chat), warn them to rotate the wallet by sending funds to a
39
+ fresh `pnl_init`-generated address.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: pnl-help
3
+ description: Show all PNL commands + a typical first-run flow. Context-aware — adjusts the suggested next step based on whether the user has a wallet and whether it's unlocked.
4
+ ---
5
+
6
+ # /pnl-help
7
+
8
+ The user wants to discover what PNL can do from the terminal.
9
+
10
+ ## What to do
11
+
12
+ 1. Call the `pnl_help` MCP tool (no arguments).
13
+ 2. Display the response verbatim — it's already formatted as a
14
+ markdown reference with context-aware suggestions.
15
+
16
+ That's it. If the user has a follow-up like "what does X do",
17
+ explain it from the table or call the specific tool directly.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: pnl-init
3
+ description: Set up PNL on this machine — generate a local Solana keypair and show the deposit address. Run this once before pitching ideas or voting.
4
+ ---
5
+
6
+ # /pnl-init
7
+
8
+ First-run setup for PNL. The user wants to start posting ideas to the
9
+ conviction-market protocol on Solana (pnl.market) from their terminal.
10
+
11
+ ## What to do
12
+
13
+ 1. Call the `pnl_init` MCP tool with no arguments.
14
+ 2. Relay the response verbatim — it contains the deposit address, current
15
+ balance, and clear funding instructions.
16
+ 3. Suggest the user fund the wallet by sending at least 0.05 SOL from any
17
+ Solana wallet (Phantom, Solflare, Backpack, an exchange withdrawal).
18
+ 4. Tell them once funded they can run `/pnl-wallet` to confirm the balance
19
+ landed, or `/pnl-pitch <idea>` to post an idea directly.
20
+
21
+ ## Tone
22
+
23
+ Be direct. Don't editorialize about non-custodial framing unless the user
24
+ asks. The terminal output already says enough.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: pnl-lock
3
+ description: Lock the PNL wallet immediately. Wipes the cached secret from memory; future signing requires a fresh unlock.
4
+ ---
5
+
6
+ # /pnl-lock
7
+
8
+ The user wants to lock the PNL wallet right now — stepping away,
9
+ ending a session, or after a sensitive operation.
10
+
11
+ ## What to do
12
+
13
+ 1. Call the `pnl_lock` MCP tool (no arguments).
14
+ 2. Confirm the wallet is locked.
15
+
16
+ This is a fast operation and safe to call any time. If the wallet was
17
+ already locked, the tool just confirms that.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: pnl-name
3
+ description: Claim or rename your PNL username. Signs a challenge with the local wallet to prove ownership — no browser, no Privy, no Gmail login needed.
4
+ ---
5
+
6
+ # /pnl-name
7
+
8
+ The user wants to set the username shown on their PNL profile and on
9
+ any market detail page they create.
10
+
11
+ ## What to do
12
+
13
+ 1. Ask the user what they'd like the username to be (or extract from
14
+ their message if they gave it inline — e.g. "/pnl-name bishwa" or
15
+ "set my pnl username to bishwa"). Constraints: 3-20 characters,
16
+ letters / numbers / `_` / `-` only.
17
+ 2. Call the `pnl_set_username` MCP tool with `{ username: <name> }`.
18
+ 3. If the response is "taken", suggest 2-3 variations (e.g. append a
19
+ number, add `_pnl`, swap an underscore for a hyphen) and ask the
20
+ user which to try.
21
+ 4. On success, relay the confirmation:
22
+ - new username
23
+ - wallet address it's attached to
24
+ - mention that this name will now show on the market detail page
25
+ for any market they create (replacing the truncated wallet address)
26
+
27
+ ## Common follow-ups
28
+
29
+ - "Why no Gmail?" → PNL is non-custodial; the wallet IS the login.
30
+ The MCP server signs a time-bounded challenge with the keypair from
31
+ pnl_init so the backend can verify ownership without a Privy session.
32
+ - "Can I change it later?" → Yes, just run `/pnl-name <new-name>` again.