@shreyassp002/pinionos-emulator 0.2.1 → 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.
package/README.md CHANGED
@@ -10,189 +10,246 @@ Test your app with the same `pinion-os` SDK calls, without real USDC spend.
10
10
 
11
11
  </div>
12
12
 
13
- ## What This Project Is
13
+ ## What This Is
14
14
 
15
- `pinionos-emulator` is a local server that mirrors Pinion skill APIs so teams can build and test products safely.
15
+ `pinionos-emulator` is a local server that mimics the real PinionOS skill API. You run it on your machine, point the `pinion-os` SDK at `localhost:4020`, and your entire app works exactly as it would in production — except every call is free, instant, and returns mock data.
16
16
 
17
- Use it when you want to:
18
- - develop app flows without hitting production
19
- - run deterministic integration tests in CI
20
- - exercise x402/unlimited-key paths locally
21
- - inspect request/response behavior in a terminal dashboard
17
+ ```text
18
+ Your app code --> pinion-os SDK --> PinionOS Emulator (localhost:4020) --> mock responses
19
+ ```
22
20
 
23
- ## Pinion SDK Compatibility
21
+ Your app code does not change. You only swap the URL.
24
22
 
25
- This emulator is designed for `pinion-os` client usage by pointing the SDK to local URL:
23
+ ## Quick Start
26
24
 
27
- ```ts
28
- import { PinionClient } from 'pinion-os';
25
+ ### 1. Install
29
26
 
30
- const client = new PinionClient({
31
- privateKey: process.env.PRIVATE_KEY!,
32
- apiUrl: 'http://localhost:4020'
33
- });
27
+ **Local install (recommended for projects):**
28
+
29
+ ```bash
30
+ npm install --save-dev @shreyassp002/pinionos-emulator
34
31
  ```
35
32
 
36
- ## Supported SDK Skills
37
-
38
- | Skill | SDK Method | Emulator Endpoint | Status |
39
- |---|---|---|---|
40
- | balance | `skills.balance(address)` | `GET /balance/:address` | Supported |
41
- | tx | `skills.tx(hash)` | `GET /tx/:hash` | Supported |
42
- | price | `skills.price(token)` | `GET /price/:token` | Supported |
43
- | wallet | `skills.wallet()` | `GET /wallet/generate` | Supported |
44
- | chat | `skills.chat(message)` | `POST /chat` | Supported |
45
- | send | `skills.send(to, amount, token)` | `POST /send` | Supported |
46
- | trade | `skills.trade(src, dst, amount, slippage)` | `POST /trade` | Supported |
47
- | fund | `skills.fund(address)` | `GET /fund/:address` | Supported |
48
- | broadcast | `skills.broadcast(tx)` | `POST /broadcast` | Supported |
49
- | unlimited | `skills.unlimited()` | `POST /unlimited` | Supported |
50
- | unlimited-verify | `skills.unlimitedVerify(key)` | `GET /unlimited/verify?key=...` | Supported |
51
-
52
- ## Supported MCP Tools
53
-
54
- Your emulator MCP server currently exposes:
55
- - `pinion_price`
56
- - `pinion_balance`
57
- - `pinion_wallet`
58
- - `pinion_tx`
59
- - `pinion_chat`
60
- - `pinion_send`
61
- - `pinion_trade`
62
- - `pinion_fund`
63
- - `pinion_broadcast`
64
- - `pinion_unlimited`
65
- - `pinion_unlimited_verify`
66
- - `pinion_pay_service`
67
- - `pinion_facilitator_verify`
68
-
69
- ## Emulator Features Beyond Core Skills
70
-
71
- - x402 middleware mode (`--x402`) with 402 challenge flow
72
- - unlimited API key issuance and verification
73
- - generic x402 test service (`/x402/*`)
74
- - mock facilitator endpoints (`/facilitator/verify`, `/facilitator/status`)
75
- - request recording (`/recording/start`, `/recording/stop`, `/recording/status`)
76
- - chaos/error injection via config (`errorSimulation`)
77
- - mutable in-memory balances + reset (`POST /reset`)
78
- - terminal dashboard (feed, prices, wallet, inspector)
33
+ **Global install:**
79
34
 
80
- ## Quick Start
35
+ ```bash
36
+ npm install -g @shreyassp002/pinionos-emulator
37
+ ```
81
38
 
82
- Install in your project:
39
+ ### 2. Start the emulator
83
40
 
84
41
  ```bash
85
- npm install @shreyassp002/pinionos-emulator
42
+ # Local install
43
+ npx pinionos-emulator
44
+
45
+ # Global install
46
+ pinionos-emulator
86
47
  ```
87
48
 
88
- Start emulator (local install):
49
+ This opens a terminal dashboard with live prices, a skill call feed, wallet info, and request inspector.
50
+
51
+ For headless/CI mode:
89
52
 
90
53
  ```bash
91
- npx pinionos-emulator
54
+ npx pinionos-emulator --no-dashboard
55
+ # or
56
+ pinionos-emulator --no-dashboard
92
57
  ```
93
58
 
94
- Health check:
59
+ ### 3. Verify it's running
95
60
 
96
61
  ```bash
97
62
  curl -s http://localhost:4020/health | jq
98
63
  ```
99
64
 
100
- Headless mode:
65
+ ### 4. Point your SDK at it
101
66
 
102
- ```bash
103
- npx pinionos-emulator --no-dashboard
67
+ ```ts
68
+ import { PinionClient } from 'pinion-os';
69
+
70
+ const client = new PinionClient({
71
+ privateKey: process.env.PRIVATE_KEY!,
72
+ apiUrl: 'http://localhost:4020' // <-- this is the only change
73
+ });
74
+
75
+ // Everything else stays the same
76
+ const price = await client.skills.price('ETH');
77
+ const wallet = await client.skills.wallet();
104
78
  ```
105
79
 
106
- Optional global install:
80
+ That's it. Your app now talks to the emulator instead of production.
81
+
82
+ ## CLI Reference
107
83
 
108
- ```bash
109
- npm install -g @shreyassp002/pinionos-emulator
110
- pinionos-emulator
111
84
  ```
85
+ pinionos-emulator [command] [options]
86
+ ```
87
+
88
+ **Commands:**
89
+
90
+ | Command | Description |
91
+ |---------|-------------|
92
+ | `start` | Start the emulator (default, can be omitted) |
93
+ | `mcp` | Start MCP stdio server (emulator must already be running) |
94
+ | `init` | Generate a starter `config.json` in the current directory |
112
95
 
113
- ## CLI Commands
96
+ **Options:**
114
97
 
115
- Local install (recommended):
98
+ | Flag | Description |
99
+ |------|-------------|
100
+ | `--port <n>` | Port to listen on (default: 4020) |
101
+ | `--x402` | Enable x402 payment simulation mode |
102
+ | `--network <name>` | `base` (default) or `base-sepolia` |
103
+ | `--no-dashboard` | Run without the terminal dashboard UI |
104
+ | `--config <path>` | Path to config.json (default: `./config.json`) |
105
+
106
+ **Examples (local install with npx):**
116
107
 
117
108
  ```bash
118
- npx pinionos-emulator start
119
- npx pinionos-emulator mcp
120
- npx pinionos-emulator init
109
+ npx pinionos-emulator # Start with defaults + dashboard
110
+ npx pinionos-emulator start # Same as above (start is default)
111
+ npx pinionos-emulator --port 3000 --x402 # Custom port + x402 mode
112
+ npx pinionos-emulator --no-dashboard # Headless for CI
113
+ npx pinionos-emulator init # Create config.json
114
+ npx pinionos-emulator mcp # Start MCP server
121
115
  ```
122
116
 
123
- Global install:
117
+ **Examples (global install):**
124
118
 
125
119
  ```bash
126
- pinionos-emulator start
127
- pinionos-emulator mcp
128
- pinionos-emulator init
120
+ pinionos-emulator # Start with defaults + dashboard
121
+ pinionos-emulator start # Same as above (start is default)
122
+ pinionos-emulator --port 3000 --x402 # Custom port + x402 mode
123
+ pinionos-emulator --no-dashboard # Headless for CI
124
+ pinionos-emulator init # Create config.json
125
+ pinionos-emulator mcp # Start MCP server
129
126
  ```
130
127
 
131
- `start` is the default command, so `npx pinionos-emulator` and `pinionos-emulator` are equivalent to `... start`.
128
+ ## Supported Skills
129
+
130
+ Every skill from the `pinion-os` SDK is supported:
131
+
132
+ | Skill | SDK Method | Emulator Endpoint |
133
+ |-------|-----------|-------------------|
134
+ | Price | `skills.price('ETH')` | `GET /price/:token` |
135
+ | Balance | `skills.balance(address)` | `GET /balance/:address` |
136
+ | Wallet | `skills.wallet()` | `GET /wallet/generate` |
137
+ | Transaction | `skills.tx(hash)` | `GET /tx/:hash` |
138
+ | Chat | `skills.chat(message, history?)` | `POST /chat` |
139
+ | Send | `skills.send(to, amount, token)` | `POST /send` |
140
+ | Trade | `skills.trade(src, dst, amount, slippage?)` | `POST /trade` |
141
+ | Fund | `skills.fund(address)` | `GET /fund/:address` |
142
+ | Broadcast | `skills.broadcast(tx, privateKey?)` | `POST /broadcast` |
143
+ | Unlimited | `skills.unlimited()` | `POST /unlimited` |
144
+ | Unlimited Verify | `skills.unlimitedVerify(key)` | `GET /unlimited/verify?key=...` |
145
+ | Catalog | `GET /catalog` | `GET /catalog` |
146
+
147
+ ## System Endpoints
148
+
149
+ These are emulator-specific endpoints for testing and debugging:
150
+
151
+ | Endpoint | Description |
152
+ |----------|-------------|
153
+ | `GET /health` | Health check |
154
+ | `POST /reset` | Reset all balances and API keys to defaults |
155
+ | `GET /catalog` | List all available skills with prices |
156
+ | `POST /recording/start` | Start recording all requests |
157
+ | `POST /recording/stop` | Stop recording |
158
+ | `GET /recording/status` | Check recording status |
159
+ | `POST /facilitator/verify` | Mock x402 facilitator verification |
160
+ | `GET /facilitator/status` | Facilitator status |
161
+ | `ALL /x402/*` | Generic x402 test service |
162
+
163
+ ## MCP Tools
164
+
165
+ For AI agents using Model Context Protocol, start the MCP server:
132
166
 
133
- Useful options:
134
- - `--port <n>`
135
- - `--x402`
136
- - `--network base|base-sepolia`
137
- - `--no-dashboard`
138
- - `--config <path>`
167
+ ```bash
168
+ npx pinionos-emulator mcp
169
+ ```
139
170
 
140
- ## Product Testing Workflow
171
+ The emulator must already be running in another terminal.
172
+
173
+ **Available tools:**
174
+
175
+ | Tool | Description |
176
+ |------|-------------|
177
+ | `pinion_setup` | Configure wallet (required before paid tools) |
178
+ | `pinion_spend_limit` | Set/check/clear spend budget |
179
+ | `pinion_catalog` | List available skills and prices |
180
+ | `pinion_price` | Get token price |
181
+ | `pinion_balance` | Get wallet balances |
182
+ | `pinion_wallet` | Generate wallet |
183
+ | `pinion_tx` | Look up transaction |
184
+ | `pinion_chat` | Chat with AI agent |
185
+ | `pinion_send` | Build send transaction |
186
+ | `pinion_trade` | Build swap transaction |
187
+ | `pinion_fund` | Get funding instructions |
188
+ | `pinion_broadcast` | Sign and broadcast transaction |
189
+ | `pinion_unlimited` | Purchase unlimited access |
190
+ | `pinion_unlimited_verify` | Verify API key |
191
+ | `pinion_pay_service` | Call generic x402 service |
192
+ | `pinion_facilitator_verify` | Verify x402 payment |
193
+
194
+ ## Configuration
195
+
196
+ Run `pinionos-emulator init` to generate a `config.json`:
197
+
198
+ ```json
199
+ {
200
+ "port": 4020,
201
+ "mockPayments": true,
202
+ "prices": {
203
+ "ETH": "useApi:coingecko",
204
+ "BTC": "useApi:coingecko",
205
+ "SOL": "useApi:coingecko",
206
+ "USDC": 1
207
+ },
208
+ "fallbackPrices": {
209
+ "ETH": 3000,
210
+ "BTC": 90000,
211
+ "SOL": 180,
212
+ "USDC": 1
213
+ },
214
+ "balances": {
215
+ "default": { "ETH": "1.5", "USDC": "250.00" }
216
+ }
217
+ }
218
+ ```
141
219
 
142
- 1. Start emulator locally.
143
- 2. Configure your app's Pinion client with `apiUrl: 'http://localhost:4020'`.
144
- 3. Run your app and test suite.
145
- 4. Assert your product behavior (not just raw route responses).
220
+ **Price resolution order:** config override -> CoinGecko API -> Binance API -> fallback prices.
146
221
 
147
- CI example:
222
+ **Price values can be:**
223
+ - A number (e.g. `1`) — fixed override
224
+ - `"useApi:coingecko"` — fetch from CoinGecko (60s cache)
225
+ - `"useApi:binance"` — fetch from Binance (60s cache)
226
+ - Absent — uses `fallbackPrices` value
148
227
 
149
- ```bash
150
- npx pinionos-emulator --no-dashboard > /tmp/pinionos-emulator.log 2>&1 & EMU_PID=$!
151
- sleep 2
152
- npm test
153
- kill $EMU_PID
154
- ```
228
+ **Custom balances:** Add wallet addresses as keys under `balances` to set specific balances. Any unknown address gets the `default` balance.
229
+
230
+ ## Emulator Features
231
+
232
+ - **Terminal dashboard** — live skill call feed, price tickers, wallet info, request inspector
233
+ - **x402 mode** — full 402 payment challenge flow with `--x402` flag
234
+ - **Unlimited keys** — issue and verify API keys via `/unlimited`
235
+ - **Request recording** — capture all requests to `pinion-requests.jsonl` for debugging
236
+ - **Error simulation** — inject failures via config for resilience testing
237
+ - **Mutable balances** — balances update on send/trade, reset with `POST /reset`
238
+ - **Skill catalog** — `GET /catalog` for agent auto-discovery
239
+ - **MCP server** — full MCP tool set with wallet setup and spend tracking
240
+
241
+ ## Notes
155
242
 
156
- ## Route Summary
157
-
158
- Core routes:
159
- - `GET /price/:token`
160
- - `GET /balance/:address`
161
- - `GET /wallet`
162
- - `GET /wallet/generate`
163
- - `GET /tx/:hash`
164
- - `POST /send`
165
- - `POST /trade`
166
- - `GET /fund/:address`
167
- - `POST /chat`
168
- - `POST /broadcast`
169
- - `GET /unlimited`
170
- - `POST /unlimited`
171
- - `GET /unlimited/verify?key=...`
172
- - `GET /unlimited/verify/:key`
173
-
174
- System routes:
175
- - `GET /`
176
- - `GET /health`
177
- - `POST /reset`
178
- - `POST /recording/start`
179
- - `POST /recording/stop`
180
- - `GET /recording/status`
181
- - `POST /facilitator/verify`
182
- - `GET /facilitator/status`
183
- - `ALL /x402/*`
184
-
185
- ## Notes On Behavior
186
-
187
- - Responses are mock/simulated with realistic structure.
188
- - Success envelope includes `mock: true` and payment metadata.
189
- - Price path uses config override -> CoinGecko -> Binance -> fallback.
190
- - This is for development/testing, not production settlement.
243
+ - All responses include `mock: true` and a simulated payment receipt.
244
+ - The success envelope spreads data at root level AND nests under `data`, so both `result.priceUSD` and `result.data.priceUSD` work.
245
+ - `/tx/:hash` is deterministic — same hash always returns the same addresses.
246
+ - `/trade` rejects same-token swaps and applies 0.3% fee + slippage.
247
+ - This is for development and testing only, not production settlement.
191
248
 
192
249
  ## Documentation
193
250
 
194
- - Detailed usage and testing guide: [user_guide.md](user_guide.md)
195
- - Planning/spec docs: [`docs/`](docs)
251
+ - Testing guide: [user_guide.md](user_guide.md)
252
+ - Planning docs: [`docs/`](docs)
196
253
 
197
254
  ## License
198
255
 
package/dist/src/app.js CHANGED
@@ -21,6 +21,7 @@ const send_1 = require("./routes/send");
21
21
  const trade_1 = require("./routes/trade");
22
22
  const facilitator_1 = require("./routes/facilitator");
23
23
  const tx_1 = __importDefault(require("./routes/tx"));
24
+ const catalog_1 = __importDefault(require("./routes/catalog"));
24
25
  const unlimited_1 = __importDefault(require("./routes/unlimited"));
25
26
  const x402service_1 = require("./routes/x402service");
26
27
  const balances_1 = require("./state/balances");
@@ -60,6 +61,7 @@ function createApp(opts = {}) {
60
61
  });
61
62
  app.use((0, paymentLogger_1.paymentLogger)(db));
62
63
  app.use((0, x402_1.x402Middleware)(db));
64
+ app.use('/catalog', catalog_1.default);
63
65
  app.get('/', (_req, res) => {
64
66
  res.json({ status: 'ok', emulator: true });
65
67
  });
@@ -40,6 +40,11 @@ class MockPinionClient {
40
40
  trade: (src, dst, amount, slippage) => this.post('/trade', { src, dst, amount, slippage: slippage ?? 1 }),
41
41
  broadcast: (tx, privateKey) => this.post('/broadcast', { tx, privateKey }),
42
42
  unlimited: () => this.post('/unlimited', {}),
43
+ catalog: async () => {
44
+ const start = Date.now();
45
+ const res = await this.http.get('/catalog');
46
+ return { status: res.status, data: res.data, paidAmount: '0', responseTimeMs: Date.now() - start };
47
+ },
43
48
  unlimitedVerify: async (key) => {
44
49
  const res = await this.http.get('/unlimited/verify', { params: { key } });
45
50
  return res.data;
@@ -8,7 +8,35 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
10
10
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
11
+ const node_crypto_1 = require("node:crypto");
11
12
  const zod_1 = require("zod");
13
+ // ── Spend tracker ──────────────────────────────────────────────────────
14
+ const spendTracker = { maxAtomic: 0, spentAtomic: 0, calls: 0, limited: false };
15
+ const SKILL_COST_ATOMIC = 10000; // $0.01 in USDC 6-decimal atomic units
16
+ const UNLIMITED_COST_ATOMIC = 100000000; // $100
17
+ function checkSpend(cost) {
18
+ if (!spendTracker.limited)
19
+ return null;
20
+ if (spendTracker.spentAtomic + cost > spendTracker.maxAtomic) {
21
+ const remaining = Math.max(0, spendTracker.maxAtomic - spendTracker.spentAtomic);
22
+ return `Spend limit exceeded. Remaining: $${(remaining / 1000000).toFixed(6)}, required: $${(cost / 1000000).toFixed(6)}`;
23
+ }
24
+ return null;
25
+ }
26
+ function recordSpend(cost) {
27
+ if (!spendTracker.limited)
28
+ return;
29
+ spendTracker.spentAtomic += cost;
30
+ spendTracker.calls += 1;
31
+ }
32
+ // ── Wallet setup state ─────────────────────────────────────────────────
33
+ let setupAddress = process.env.PINION_PRIVATE_KEY ? '0x_from_env' : null;
34
+ function requireSetup() {
35
+ if (!setupAddress) {
36
+ return 'Wallet not configured. Call pinion_setup first with action "import" or "generate".';
37
+ }
38
+ return null;
39
+ }
12
40
  function normalizeBaseUrl(baseUrl) {
13
41
  return baseUrl.replace(/\/+$/, '');
14
42
  }
@@ -54,11 +82,82 @@ async function main() {
54
82
  // Use the typed tool registration API
55
83
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
84
  const tool = server.tool.bind(server);
57
- tool('pinion_price', 'Get the current USD price for a token (ETH, BTC, SOL, USDC, MATIC). Returns priceUSD and 24h change.', { token: zod_1.z.string().describe('Token symbol, e.g. ETH') }, async ({ token }) => wrapResult(await get(`/price/${token}`)));
58
- tool('pinion_balance', 'Get ETH and USDC balances for a Base wallet address.', { address: zod_1.z.string().describe('Ethereum/Base wallet address (0x...)') }, async ({ address }) => wrapResult(await get(`/balance/${address}`)));
59
- tool('pinion_wallet', 'Generate a fresh Base wallet keypair (address + private key). For testing only — never use for real funds.', {}, async () => wrapResult(await get('/wallet/generate')));
60
- tool('pinion_tx', 'Look up decoded transaction details for a Base tx hash.', { hash: zod_1.z.string().describe('Transaction hash (0x...)') }, async ({ hash }) => wrapResult(await get(`/tx/${hash}`)));
61
- tool('pinion_chat', 'Chat with the Pinion AI agent. Pass a message and optionally conversation history.', {
85
+ /** Wraps a paid tool handler with setup + spend checks */
86
+ function paidTool(name, description, schema, handler, cost = SKILL_COST_ATOMIC) {
87
+ tool(name, description, schema, async (args) => {
88
+ const setupErr = requireSetup();
89
+ if (setupErr)
90
+ return wrapResult({ error: setupErr });
91
+ const spendErr = checkSpend(cost);
92
+ if (spendErr)
93
+ return wrapResult({ error: spendErr });
94
+ const result = await handler(args);
95
+ recordSpend(cost);
96
+ return result;
97
+ });
98
+ }
99
+ // ── Free tools ──────────────────────────────────────────────────────
100
+ tool('pinion_setup', 'Configure wallet for PinionOS. Required before using paid tools.', {
101
+ action: zod_1.z.enum(['import', 'generate']).describe('"import" to use an existing key, "generate" to create a new one'),
102
+ private_key: zod_1.z.string().optional().describe('Private key (required for "import")')
103
+ }, async ({ action, private_key }) => {
104
+ if (action === 'import') {
105
+ if (!private_key || !private_key.startsWith('0x') || private_key.length < 66) {
106
+ return wrapResult({ error: 'Invalid private key. Must be a 0x-prefixed 64-character hex string.' });
107
+ }
108
+ setupAddress = `0x${(0, node_crypto_1.randomBytes)(20).toString('hex')}`;
109
+ return wrapResult({ status: 'ok', address: setupAddress, action: 'imported' });
110
+ }
111
+ // generate
112
+ const addr = `0x${(0, node_crypto_1.randomBytes)(20).toString('hex')}`;
113
+ const key = `0x${(0, node_crypto_1.randomBytes)(32).toString('hex')}`;
114
+ setupAddress = addr;
115
+ return wrapResult({ status: 'ok', address: addr, privateKey: key, action: 'generated' });
116
+ });
117
+ tool('pinion_spend_limit', 'Manage spend budget for PinionOS skill calls. Actions: set, status, clear.', {
118
+ action: zod_1.z.enum(['set', 'status', 'clear']).describe('Action to perform'),
119
+ max_usdc: zod_1.z.string().optional().describe('Max budget in USDC (required for "set"), e.g. "0.05"')
120
+ }, async ({ action, max_usdc }) => {
121
+ if (action === 'set') {
122
+ if (!max_usdc)
123
+ return wrapResult({ error: 'max_usdc is required for "set" action' });
124
+ const parsed = parseFloat(max_usdc);
125
+ if (isNaN(parsed) || parsed <= 0)
126
+ return wrapResult({ error: 'max_usdc must be a positive number' });
127
+ spendTracker.maxAtomic = Math.round(parsed * 1000000);
128
+ spendTracker.spentAtomic = 0;
129
+ spendTracker.calls = 0;
130
+ spendTracker.limited = true;
131
+ return wrapResult({ status: 'ok', maxBudget: max_usdc, message: `Spend limit set to $${max_usdc} USDC` });
132
+ }
133
+ if (action === 'clear') {
134
+ spendTracker.limited = false;
135
+ spendTracker.maxAtomic = 0;
136
+ spendTracker.spentAtomic = 0;
137
+ spendTracker.calls = 0;
138
+ return wrapResult({ status: 'ok', message: 'Spend limit cleared' });
139
+ }
140
+ // status
141
+ const max = spendTracker.limited ? (spendTracker.maxAtomic / 1000000).toFixed(6) : 'unlimited';
142
+ const spent = (spendTracker.spentAtomic / 1000000).toFixed(6);
143
+ const remaining = spendTracker.limited
144
+ ? (Math.max(0, spendTracker.maxAtomic - spendTracker.spentAtomic) / 1000000).toFixed(6)
145
+ : 'unlimited';
146
+ return wrapResult({
147
+ maxBudget: max,
148
+ spent,
149
+ remaining,
150
+ callCount: spendTracker.calls,
151
+ isLimited: spendTracker.limited
152
+ });
153
+ });
154
+ tool('pinion_catalog', 'List all available PinionOS skills with prices and descriptions.', {}, async () => wrapResult(await get('/catalog')));
155
+ // ── Paid tools ──────────────────────────────────────────────────────
156
+ paidTool('pinion_price', 'Get the current USD price for a token (ETH, BTC, SOL, USDC, MATIC). Returns priceUSD and 24h change.', { token: zod_1.z.string().describe('Token symbol, e.g. ETH') }, async ({ token }) => wrapResult(await get(`/price/${token}`)));
157
+ paidTool('pinion_balance', 'Get ETH and USDC balances for a Base wallet address.', { address: zod_1.z.string().describe('Ethereum/Base wallet address (0x...)') }, async ({ address }) => wrapResult(await get(`/balance/${address}`)));
158
+ paidTool('pinion_wallet', 'Generate a fresh Base wallet keypair (address + private key). For testing only — never use for real funds.', {}, async () => wrapResult(await get('/wallet/generate')));
159
+ paidTool('pinion_tx', 'Look up decoded transaction details for a Base tx hash.', { hash: zod_1.z.string().describe('Transaction hash (0x...)') }, async ({ hash }) => wrapResult(await get(`/tx/${hash}`)));
160
+ paidTool('pinion_chat', 'Chat with the Pinion AI agent. Pass a message and optionally conversation history.', {
62
161
  message: zod_1.z.string().describe('The user message to send'),
63
162
  history: zod_1.z
64
163
  .array(zod_1.z.object({ role: zod_1.z.string(), content: zod_1.z.string() }))
@@ -70,19 +169,19 @@ async function main() {
70
169
  : [{ role: 'user', content: message }];
71
170
  return wrapResult(await post('/chat', { messages }));
72
171
  });
73
- tool('pinion_send', 'Build an unsigned ETH or ERC-20 transfer transaction. Sign and broadcast with pinion_broadcast.', {
172
+ paidTool('pinion_send', 'Build an unsigned ETH or ERC-20 transfer transaction. Sign and broadcast with pinion_broadcast.', {
74
173
  to: zod_1.z.string().describe('Recipient address (0x...)'),
75
174
  amount: zod_1.z.string().describe('Amount to send, e.g. "0.01"'),
76
175
  token: zod_1.z.string().describe('Token to send (ETH, USDC, WETH, DAI, WBTC, CBETH)')
77
176
  }, async (args) => wrapResult(await post('/send', args)));
78
- tool('pinion_trade', 'Get an unsigned swap transaction via Uniswap V3 on Base. Sign and broadcast with pinion_broadcast.', {
177
+ paidTool('pinion_trade', 'Get an unsigned swap transaction via Uniswap V3 on Base. Sign and broadcast with pinion_broadcast.', {
79
178
  src: zod_1.z.string().describe('Source token symbol, e.g. ETH'),
80
179
  dst: zod_1.z.string().describe('Destination token symbol, e.g. USDC'),
81
180
  amount: zod_1.z.string().describe('Amount of src token to swap'),
82
181
  slippage: zod_1.z.number().optional().describe('Slippage tolerance in percent, default 1')
83
182
  }, async (args) => wrapResult(await post('/trade', args)));
84
- tool('pinion_fund', 'Get wallet balance and funding instructions for a Base address. Includes deposit address and bridging steps.', { address: zod_1.z.string().describe('Wallet address to check and get funding info for') }, async ({ address }) => wrapResult(await get(`/fund/${address}`)));
85
- tool('pinion_broadcast', 'Sign and broadcast a transaction on Base. Pass the unsigned tx object from pinion_send or pinion_trade.', {
183
+ paidTool('pinion_fund', 'Get wallet balance and funding instructions for a Base address. Includes deposit address and bridging steps.', { address: zod_1.z.string().describe('Wallet address to check and get funding info for') }, async ({ address }) => wrapResult(await get(`/fund/${address}`)));
184
+ paidTool('pinion_broadcast', 'Sign and broadcast a transaction on Base. Pass the unsigned tx object from pinion_send or pinion_trade.', {
86
185
  tx: zod_1.z
87
186
  .object({
88
187
  to: zod_1.z.string().describe('Transaction recipient'),
@@ -93,7 +192,7 @@ async function main() {
93
192
  .describe('Unsigned transaction object'),
94
193
  privateKey: zod_1.z.string().optional().describe('Private key to sign with (uses emulator default if omitted)')
95
194
  }, async (args) => wrapResult(await post('/broadcast', args)));
96
- tool('pinion_unlimited', 'Purchase (simulated) unlimited access to all Pinion OS skills. Returns an API key for X-API-KEY header.', {}, async () => wrapResult(await post('/unlimited', {})));
195
+ paidTool('pinion_unlimited', 'Purchase (simulated) unlimited access to all Pinion OS skills. Returns an API key for X-API-KEY header.', {}, async () => wrapResult(await post('/unlimited', {})), UNLIMITED_COST_ATOMIC);
97
196
  tool('pinion_unlimited_verify', 'Verify whether an unlimited API key is valid. Returns validity status and associated address.', { key: zod_1.z.string().describe('The API key to verify (from pinion_unlimited)') }, async ({ key }) => {
98
197
  const res = await axios_1.default.get(`${API}/unlimited/verify`, { params: { key } });
99
198
  return wrapResult(res.data);
@@ -59,7 +59,7 @@ function parsePaymentHeader(header) {
59
59
  }
60
60
  }
61
61
  // Routes that skip x402 (free endpoints, health checks, etc.)
62
- const SKIP_PATHS = new Set(['/', '/health', '/reset']);
62
+ const SKIP_PATHS = new Set(['/', '/health', '/reset', '/catalog']);
63
63
  const SKIP_PREFIXES = ['/unlimited/verify', '/facilitator', '/x402/', '/recording'];
64
64
  function shouldSkip(path, method) {
65
65
  if (method === 'OPTIONS')
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const express_1 = require("express");
4
+ const config_1 = require("../config");
5
+ const MOCK_PAY_TO = '0x000000000000000000000000000000000000dead';
6
+ const SKILLS = [
7
+ { name: 'balance', path: '/balance/:address', method: 'GET', description: 'Get wallet balances' },
8
+ { name: 'tx', path: '/tx/:hash', method: 'GET', description: 'Look up transaction details' },
9
+ { name: 'price', path: '/price/:token', method: 'GET', description: 'Get token price in USD' },
10
+ { name: 'wallet', path: '/wallet/generate', method: 'GET', description: 'Generate a new wallet' },
11
+ { name: 'chat', path: '/chat', method: 'POST', description: 'Chat with Pinion AI agent' },
12
+ { name: 'send', path: '/send', method: 'POST', description: 'Build a token transfer transaction' },
13
+ { name: 'trade', path: '/trade', method: 'POST', description: 'Build a swap transaction' },
14
+ { name: 'fund', path: '/fund/:address', method: 'GET', description: 'Get funding instructions' },
15
+ { name: 'broadcast', path: '/broadcast', method: 'POST', description: 'Sign and broadcast a transaction' },
16
+ ];
17
+ const catalogRouter = (0, express_1.Router)();
18
+ catalogRouter.get('/', (_req, res) => {
19
+ const net = (0, config_1.getNetworkInfo)();
20
+ res.json({
21
+ skills: SKILLS.map((s) => ({
22
+ ...s,
23
+ price: '0.01',
24
+ priceToken: 'USDC',
25
+ })),
26
+ payTo: MOCK_PAY_TO,
27
+ network: net.name,
28
+ mock: true,
29
+ });
30
+ });
31
+ exports.default = catalogRouter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shreyassp002/pinionos-emulator",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Local PinionOS emulator for free agent development",
5
5
  "main": "dist/src/emulator.js",
6
6
  "bin": {