@surf-ai/sdk 0.1.5-beta → 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 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, method: options?.arrayMode ? "all" : "execute" });
94
+ return post("db/query", { sql, params, arrayMode: options?.arrayMode ?? false });
95
95
  }
96
96
  async function dbTables() {
97
97
  return get("db/tables");
@@ -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;
@@ -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, method: options?.arrayMode ? "all" : "execute" });
64
+ return post("db/query", { sql, params, arrayMode: options?.arrayMode ?? false });
65
65
  }
66
66
  async function dbTables() {
67
67
  return get("db/tables");