@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 +193 -136
- package/dist/src/app.js +2 -0
- package/dist/src/client/MockPinionClient.js +5 -0
- package/dist/src/mcp/server.js +109 -10
- package/dist/src/middleware/x402.js +1 -1
- package/dist/src/routes/catalog.js +31 -0
- package/package.json +1 -1
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
|
|
13
|
+
## What This Is
|
|
14
14
|
|
|
15
|
-
`pinionos-emulator` is a local server that
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
21
|
+
Your app code does not change. You only swap the URL.
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
## Quick Start
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
import { PinionClient } from 'pinion-os';
|
|
25
|
+
### 1. Install
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
**Local install (recommended for projects):**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --save-dev @shreyassp002/pinionos-emulator
|
|
34
31
|
```
|
|
35
32
|
|
|
36
|
-
|
|
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
|
-
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g @shreyassp002/pinionos-emulator
|
|
37
|
+
```
|
|
81
38
|
|
|
82
|
-
|
|
39
|
+
### 2. Start the emulator
|
|
83
40
|
|
|
84
41
|
```bash
|
|
85
|
-
|
|
42
|
+
# Local install
|
|
43
|
+
npx pinionos-emulator
|
|
44
|
+
|
|
45
|
+
# Global install
|
|
46
|
+
pinionos-emulator
|
|
86
47
|
```
|
|
87
48
|
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
### 4. Point your SDK at it
|
|
101
66
|
|
|
102
|
-
```
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
**Options:**
|
|
114
97
|
|
|
115
|
-
|
|
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
|
|
119
|
-
npx pinionos-emulator
|
|
120
|
-
npx pinionos-emulator
|
|
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
|
-
|
|
117
|
+
**Examples (global install):**
|
|
124
118
|
|
|
125
119
|
```bash
|
|
126
|
-
pinionos-emulator
|
|
127
|
-
pinionos-emulator
|
|
128
|
-
pinionos-emulator
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
- `--network base|base-sepolia`
|
|
137
|
-
- `--no-dashboard`
|
|
138
|
-
- `--config <path>`
|
|
167
|
+
```bash
|
|
168
|
+
npx pinionos-emulator mcp
|
|
169
|
+
```
|
|
139
170
|
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- `
|
|
160
|
-
-
|
|
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
|
-
-
|
|
195
|
-
- Planning
|
|
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;
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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;
|