@surf-ai/sdk 0.1.4 → 0.1.6-beta
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 +223 -0
- package/dist/db/index.cjs +1 -1
- package/dist/db/index.d.cts +3 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.js +1 -1
- package/dist/react/index.d.ts +284 -84
- package/dist/react/index.js +148 -65
- package/dist/server/index.cjs +113 -89
- package/dist/server/index.d.cts +195 -1
- package/dist/server/index.d.ts +195 -1
- package/dist/server/index.js +111 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,6 +168,229 @@ Public (local dev):
|
|
|
168
168
|
| `kalshi` | `events`, `markets`, `prices`, `volumes` |
|
|
169
169
|
| `prediction_market` | `category_metrics` |
|
|
170
170
|
|
|
171
|
+
## Database
|
|
172
|
+
|
|
173
|
+
Per-user PostgreSQL (Neon) with Drizzle ORM. Auto-provisioned, auto-synced on server startup.
|
|
174
|
+
|
|
175
|
+
### Setup
|
|
176
|
+
|
|
177
|
+
Define tables in `backend/db/schema.js`:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const { pgTable, serial, text, integer, boolean, timestamp, real, jsonb } = require('drizzle-orm/pg-core')
|
|
181
|
+
|
|
182
|
+
exports.users = pgTable('users', {
|
|
183
|
+
id: serial('id').primaryKey(),
|
|
184
|
+
name: text('name').notNull(),
|
|
185
|
+
email: text('email'),
|
|
186
|
+
created_at: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Tables are auto-created when the server starts and when `schema.js` changes (file watcher). You can also call `POST /api/__sync-schema` explicitly.
|
|
191
|
+
|
|
192
|
+
### Querying (in backend routes)
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
// backend/routes/users.js
|
|
196
|
+
const { drizzle } = require('drizzle-orm/neon-http')
|
|
197
|
+
const { dbQuery } = require('@surf-ai/sdk/db')
|
|
198
|
+
const { eq, desc, count } = require('drizzle-orm')
|
|
199
|
+
const schema = require('../db/schema')
|
|
200
|
+
|
|
201
|
+
// IMPORTANT: arrayMode must be true for Drizzle to work correctly
|
|
202
|
+
const db = drizzle(async (sql, params, method) => {
|
|
203
|
+
const result = await dbQuery(sql, params, { arrayMode: true })
|
|
204
|
+
return { rows: result.rows || [] }
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
router.get('/', async (req, res) => {
|
|
208
|
+
const users = await db.select().from(schema.users).orderBy(desc(schema.users.created_at)).limit(20)
|
|
209
|
+
res.json(users)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
router.post('/', async (req, res) => {
|
|
213
|
+
const [user] = await db.insert(schema.users).values(req.body).returning()
|
|
214
|
+
res.json(user)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
router.patch('/:id', async (req, res) => {
|
|
218
|
+
const [user] = await db.update(schema.users).set(req.body).where(eq(schema.users.id, +req.params.id)).returning()
|
|
219
|
+
res.json(user)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
router.delete('/:id', async (req, res) => {
|
|
223
|
+
await db.delete(schema.users).where(eq(schema.users.id, +req.params.id))
|
|
224
|
+
res.json({ ok: true })
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Raw SQL (escape hatch)
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
const { dbQuery } = require('@surf-ai/sdk/db')
|
|
232
|
+
const result = await dbQuery('SELECT symbol, SUM(volume) FROM trades GROUP BY symbol ORDER BY 2 DESC LIMIT $1', [20])
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### DB Proxy Endpoints
|
|
236
|
+
|
|
237
|
+
| Method | Path | Purpose |
|
|
238
|
+
|--------|------|---------|
|
|
239
|
+
| POST | `/proxy/db/provision` | Create database (idempotent) |
|
|
240
|
+
| POST | `/proxy/db/query` | Execute SQL query |
|
|
241
|
+
| GET | `/proxy/db/tables` | List all tables |
|
|
242
|
+
| GET | `/proxy/db/table-schema?table=X` | Column definitions for table X |
|
|
243
|
+
| GET | `/proxy/db/status` | Connection status |
|
|
244
|
+
| POST | `/api/__sync-schema` | Force schema sync from `db/schema.js` |
|
|
245
|
+
|
|
246
|
+
### Safety Rules
|
|
247
|
+
|
|
248
|
+
- **NEVER** `DROP TABLE` or `TRUNCATE` with existing data — use `ALTER TABLE ADD COLUMN IF NOT EXISTS`
|
|
249
|
+
- **NEVER** `DELETE FROM` without `WHERE`
|
|
250
|
+
- **Always** call `GET /proxy/db/tables` before creating tables — check what exists first
|
|
251
|
+
- **Never** seed data into non-empty tables — check row count first
|
|
252
|
+
- Limits: 30s query timeout, 5000 max rows, 50 max tables
|
|
253
|
+
|
|
254
|
+
## Cron Jobs
|
|
255
|
+
|
|
256
|
+
Built-in cron system powered by `croner`. Managed via `cron.json` + handler files.
|
|
257
|
+
|
|
258
|
+
### When to Use
|
|
259
|
+
|
|
260
|
+
- **Side effects** (DB writes, alerts, cache refresh) → cron job
|
|
261
|
+
- **Display refresh** (show latest price) → `useQuery({ refetchInterval: 30000 })`
|
|
262
|
+
|
|
263
|
+
### Setup
|
|
264
|
+
|
|
265
|
+
1. Create `backend/cron.json`:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
[
|
|
269
|
+
{
|
|
270
|
+
"id": "refresh-prices",
|
|
271
|
+
"name": "Refresh token prices",
|
|
272
|
+
"schedule": "*/5 * * * *",
|
|
273
|
+
"handler": "tasks/refresh-prices.js",
|
|
274
|
+
"enabled": true,
|
|
275
|
+
"timeout": 120
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
2. Create handler in `backend/tasks/`:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
// backend/tasks/refresh-prices.js
|
|
284
|
+
const { dataApi } = require('@surf-ai/sdk/server')
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
async handler() {
|
|
288
|
+
const data = await dataApi.market.price({ symbol: 'BTC' })
|
|
289
|
+
// process and store...
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Cron fields
|
|
295
|
+
|
|
296
|
+
| Field | Type | Required | Description |
|
|
297
|
+
|-------|------|----------|-------------|
|
|
298
|
+
| `id` | string | yes | Unique identifier |
|
|
299
|
+
| `name` | string | yes | Display name |
|
|
300
|
+
| `schedule` | string | yes | Cron expression (min 1-minute interval) |
|
|
301
|
+
| `handler` | string | yes | Path from `backend/` to handler file |
|
|
302
|
+
| `enabled` | boolean | yes | Active or not |
|
|
303
|
+
| `timeout` | number | no | Max seconds (default 300) |
|
|
304
|
+
|
|
305
|
+
### Common schedules
|
|
306
|
+
|
|
307
|
+
| Expression | Meaning |
|
|
308
|
+
|-----------|---------|
|
|
309
|
+
| `*/5 * * * *` | Every 5 minutes |
|
|
310
|
+
| `0 * * * *` | Every hour |
|
|
311
|
+
| `0 0 * * *` | Daily at midnight |
|
|
312
|
+
| `0 9 * * 1` | Monday at 9 AM |
|
|
313
|
+
|
|
314
|
+
### Management API
|
|
315
|
+
|
|
316
|
+
| Method | Path | Purpose |
|
|
317
|
+
|--------|------|---------|
|
|
318
|
+
| GET | `/api/cron` | List all jobs with status |
|
|
319
|
+
| POST | `/api/cron` | Create/update jobs (updates cron.json) |
|
|
320
|
+
| PATCH | `/api/cron/:id` | Update a single job |
|
|
321
|
+
| DELETE | `/api/cron/:id` | Remove a job |
|
|
322
|
+
| POST | `/api/cron/:id/run` | Manually trigger a job |
|
|
323
|
+
|
|
324
|
+
Rules: handlers must be idempotent, never use `setInterval` in server.js, `croner` is pre-installed (never `npm install` it).
|
|
325
|
+
|
|
326
|
+
## Web Search & Fetch
|
|
327
|
+
|
|
328
|
+
Search the web and scrape pages through the data proxy:
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
// Backend
|
|
332
|
+
const { dataApi } = require('@surf-ai/sdk/server')
|
|
333
|
+
|
|
334
|
+
// Search
|
|
335
|
+
const results = await dataApi.search.web({ q: 'BTC ETF approval', limit: 10 })
|
|
336
|
+
|
|
337
|
+
// Fetch page as markdown
|
|
338
|
+
const page = await dataApi.web.fetch({ url: 'https://example.com', target_selector: '.article' })
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
// Frontend
|
|
343
|
+
import { useSearchWeb, useWebFetch } from '@surf-ai/sdk/react'
|
|
344
|
+
|
|
345
|
+
const { data } = useSearchWeb({ q: 'BTC ETF', limit: 5 })
|
|
346
|
+
const { data: page } = useWebFetch({ url: 'https://example.com' })
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Search params: `q` (required), `limit`, `offset`, `site` (domain filter). Fetch params: `url` (required), `target_selector`, `remove_selector`, `timeout`.
|
|
350
|
+
|
|
351
|
+
## Data Strategy
|
|
352
|
+
|
|
353
|
+
### Market vs Exchange
|
|
354
|
+
|
|
355
|
+
- **`market`** = aggregated cross-exchange (market cap, total OI, Fear & Greed, ETF flows). Use for: "show BTC price", "market overview"
|
|
356
|
+
- **`exchange`** = per-exchange real-time (order book, klines, funding rate). Use for: "Binance BTC order book", "compare funding rates"
|
|
357
|
+
|
|
358
|
+
| Need | Use |
|
|
359
|
+
|------|-----|
|
|
360
|
+
| Price history, rankings, sentiment | `market` |
|
|
361
|
+
| Total derivatives OI, liquidations, ETF flows | `market` |
|
|
362
|
+
| Order book, klines from a specific exchange | `exchange` |
|
|
363
|
+
| Funding rate, long/short for specific pair | `exchange` |
|
|
364
|
+
|
|
365
|
+
### Data complexity tiers
|
|
366
|
+
|
|
367
|
+
| Complexity | Approach |
|
|
368
|
+
|-----------|----------|
|
|
369
|
+
| Single endpoint, read-only | Frontend hook directly (`useMarketPrice`) |
|
|
370
|
+
| Combine multiple endpoints | Backend route with `Promise.all` + multiple `dataApi` calls |
|
|
371
|
+
| External API not in proxy | Backend route + `process.env` for API keys |
|
|
372
|
+
| On-chain SQL analytics | `dataApi.onchain.sql()` (see `onchain` skill for ClickHouse schema) |
|
|
373
|
+
|
|
374
|
+
### Backend composition pattern
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
// backend/routes/overview.js — combine multiple data sources
|
|
378
|
+
const { dataApi } = require('@surf-ai/sdk/server')
|
|
379
|
+
const router = require('express').Router()
|
|
380
|
+
|
|
381
|
+
router.get('/', async (req, res) => {
|
|
382
|
+
const { symbol } = req.query
|
|
383
|
+
const [price, holders, social] = await Promise.all([
|
|
384
|
+
dataApi.market.price({ symbol }),
|
|
385
|
+
dataApi.token.holders({ address: req.query.address, chain: 'ethereum', limit: 10 }),
|
|
386
|
+
dataApi.social.detail({ username: req.query.twitter }),
|
|
387
|
+
])
|
|
388
|
+
res.json({ price: price.data?.[0], topHolders: holders.data, social: social.data })
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
module.exports = router
|
|
392
|
+
```
|
|
393
|
+
|
|
171
394
|
## Codegen
|
|
172
395
|
|
|
173
396
|
API methods and React hooks are auto-generated from hermod's OpenAPI spec via the surf CLI:
|
package/dist/db/index.cjs
CHANGED
|
@@ -91,7 +91,7 @@ async function dbProvision() {
|
|
|
91
91
|
return post("db/provision");
|
|
92
92
|
}
|
|
93
93
|
async function dbQuery(sql, params, options) {
|
|
94
|
-
return post("db/query", { sql, params,
|
|
94
|
+
return post("db/query", { sql, params, arrayMode: options?.arrayMode ?? false });
|
|
95
95
|
}
|
|
96
96
|
async function dbTables() {
|
|
97
97
|
return get("db/tables");
|
package/dist/db/index.d.cts
CHANGED
|
@@ -25,6 +25,9 @@ declare function dbProvision(): Promise<{
|
|
|
25
25
|
/**
|
|
26
26
|
* Execute a SQL query via /proxy/db/query.
|
|
27
27
|
* Uses pg-proxy driver under the hood — Drizzle ORM calls this automatically.
|
|
28
|
+
*
|
|
29
|
+
* @param options.arrayMode - When true, rows are returned as positional arrays
|
|
30
|
+
* instead of objects. Required for Drizzle ORM pg-proxy compatibility.
|
|
28
31
|
*/
|
|
29
32
|
declare function dbQuery(sql: string, params?: any[], options?: {
|
|
30
33
|
arrayMode?: boolean;
|
package/dist/db/index.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ declare function dbProvision(): Promise<{
|
|
|
25
25
|
/**
|
|
26
26
|
* Execute a SQL query via /proxy/db/query.
|
|
27
27
|
* Uses pg-proxy driver under the hood — Drizzle ORM calls this automatically.
|
|
28
|
+
*
|
|
29
|
+
* @param options.arrayMode - When true, rows are returned as positional arrays
|
|
30
|
+
* instead of objects. Required for Drizzle ORM pg-proxy compatibility.
|
|
28
31
|
*/
|
|
29
32
|
declare function dbQuery(sql: string, params?: any[], options?: {
|
|
30
33
|
arrayMode?: boolean;
|
package/dist/db/index.js
CHANGED
|
@@ -61,7 +61,7 @@ async function dbProvision() {
|
|
|
61
61
|
return post("db/provision");
|
|
62
62
|
}
|
|
63
63
|
async function dbQuery(sql, params, options) {
|
|
64
|
-
return post("db/query", { sql, params,
|
|
64
|
+
return post("db/query", { sql, params, arrayMode: options?.arrayMode ?? false });
|
|
65
65
|
}
|
|
66
66
|
async function dbTables() {
|
|
67
67
|
return get("db/tables");
|