@surf-ai/sdk 0.1.6-beta → 1.0.0-alpha.1
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 +86 -375
- package/dist/db/index.cjs +216 -39
- package/dist/db/index.d.cts +37 -3
- package/dist/db/index.d.ts +37 -3
- package/dist/db/index.js +210 -38
- package/dist/server/index.cjs +250 -268
- package/dist/server/index.d.cts +9 -15
- package/dist/server/index.d.ts +9 -15
- package/dist/server/index.js +269 -195
- package/package.json +6 -29
- package/dist/chunk-J4OMYO3F.js +0 -70
- package/dist/client-3YMIRPDV.js +0 -8
- package/dist/react/index.d.ts +0 -3650
- package/dist/react/index.js +0 -1175
- package/src/theme/index.css +0 -314
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @surf-ai/sdk
|
|
2
2
|
|
|
3
|
-
Surf
|
|
3
|
+
Surf SDK 1.0 for backend apps, typed Surf data access, and database helpers.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,425 +8,136 @@ Surf platform SDK — data API client, Express server runtime, React hooks, and
|
|
|
8
8
|
bun add @surf-ai/sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Configuration
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
SDK 1.0 uses a single direct-auth model.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div className={cn('p-4', isLoading && 'opacity-50')}>
|
|
22
|
-
BTC: ${data?.data?.[0]?.value}
|
|
23
|
-
</div>
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Backend (data API)
|
|
29
|
-
|
|
30
|
-
```js
|
|
31
|
-
const { dataApi } = require('@surf-ai/sdk/server')
|
|
15
|
+
| Env Var | Default | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `SURF_API_BASE_URL` | `https://api.ask.surf/gateway/v1` | Full Surf API base URL |
|
|
18
|
+
| `SURF_API_KEY` | none | Bearer token used for upstream requests and protected runtime endpoints |
|
|
19
|
+
| `PORT` | none | Express server port when `createServer({ port })` is not provided |
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
const btc = await dataApi.market.price({ symbol: 'BTC', time_range: '1d' })
|
|
35
|
-
const holders = await dataApi.token.holders({ address: '0x...', chain: 'ethereum' })
|
|
36
|
-
const trades = await dataApi.onchain.sql({ sql: 'SELECT ...', max_rows: 100 })
|
|
21
|
+
All upstream SDK requests use:
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
```http
|
|
24
|
+
Authorization: Bearer <SURF_API_KEY>
|
|
40
25
|
```
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
## Subpath exports
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
createServer({ port: 3001 }).start()
|
|
49
|
-
```
|
|
29
|
+
| Import | What it provides |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `@surf-ai/sdk/server` | `createServer()`, `dataApi` |
|
|
32
|
+
| `@surf-ai/sdk/db` | `dbProvision()`, `dbQuery()`, `dbTables()`, `dbTableSchema()`, `dbStatus()` |
|
|
50
33
|
|
|
51
|
-
|
|
34
|
+
## Data API usage
|
|
52
35
|
|
|
53
36
|
```js
|
|
54
|
-
// routes/btc.js → /api/btc
|
|
55
37
|
const { dataApi } = require('@surf-ai/sdk/server')
|
|
56
|
-
const router = require('express').Router()
|
|
57
38
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
const btc = await dataApi.market.price({ symbol: 'BTC', time_range: '1d' })
|
|
40
|
+
const holders = await dataApi.token.holders({
|
|
41
|
+
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
|
42
|
+
chain: 'ethereum',
|
|
61
43
|
})
|
|
62
44
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
## Subpath Exports
|
|
67
|
-
|
|
68
|
-
| Import | What |
|
|
69
|
-
|--------|------|
|
|
70
|
-
| `@surf-ai/sdk/server` | `createServer()`, `dataApi` — Express runtime + typed data API |
|
|
71
|
-
| `@surf-ai/sdk/react` | `useMarketPrice()`, `cn()`, `useToast()` — React hooks + utilities |
|
|
72
|
-
| `@surf-ai/sdk/db` | `dbQuery()`, `dbProvision()`, `dbTables()` — Drizzle/Neon database |
|
|
73
|
-
|
|
74
|
-
## Built-in Endpoints
|
|
75
|
-
|
|
76
|
-
`createServer()` provides these automatically:
|
|
77
|
-
|
|
78
|
-
| Endpoint | Method | Purpose |
|
|
79
|
-
|----------|--------|---------|
|
|
80
|
-
| `/api/health` | GET | Health check — `{ status: 'ok' }` |
|
|
81
|
-
| `/api/__sync-schema` | POST | Sync `db/schema.js` tables to database |
|
|
82
|
-
| `/api/cron` | GET | List cron jobs and status |
|
|
83
|
-
| `/api/cron` | POST | Update cron.json and reload |
|
|
84
|
-
| `/proxy/*` | ANY | Data API passthrough to hermod |
|
|
85
|
-
|
|
86
|
-
Routes in `routes/*.js` are auto-loaded as `/api/{filename}`.
|
|
87
|
-
|
|
88
|
-
DB schema is auto-synced on startup and when `db/schema.js` changes (file watcher).
|
|
89
|
-
|
|
90
|
-
## Environment Variables
|
|
91
|
-
|
|
92
|
-
The SDK auto-detects the runtime mode from environment variables. New prefixed names take priority; legacy names are supported for backward compatibility.
|
|
93
|
-
|
|
94
|
-
### Data API routing
|
|
95
|
-
|
|
96
|
-
| Env Var | Legacy | Mode | Set By |
|
|
97
|
-
|---------|--------|------|--------|
|
|
98
|
-
| `SURF_SANDBOX_PROXY_BASE` | `DATA_PROXY_BASE` | Sandbox | urania executor |
|
|
99
|
-
| `SURF_DEPLOYED_GATEWAY_URL` | `GATEWAY_URL` | Deployed | Bifrost |
|
|
100
|
-
| `SURF_DEPLOYED_APP_TOKEN` | `APP_TOKEN` | Deployed | Bifrost |
|
|
101
|
-
|
|
102
|
-
**Routing logic:**
|
|
103
|
-
```
|
|
104
|
-
if SURF_SANDBOX_PROXY_BASE → sandbox (OutboundProxy handles auth)
|
|
105
|
-
elif SURF_DEPLOYED_GATEWAY_URL + SURF_DEPLOYED_APP_TOKEN → deployed (hermod with Bearer)
|
|
106
|
-
else → public (api.ask.surf, no auth)
|
|
45
|
+
// Escape hatch for endpoints that do not have a typed helper yet.
|
|
46
|
+
const custom = await dataApi.get('market/price', { symbol: 'ETH', time_range: '1d' })
|
|
107
47
|
```
|
|
108
48
|
|
|
109
|
-
|
|
49
|
+
Available categories include:
|
|
110
50
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
51
|
+
- `market`
|
|
52
|
+
- `token`
|
|
53
|
+
- `wallet`
|
|
54
|
+
- `onchain`
|
|
55
|
+
- `social`
|
|
56
|
+
- `project`
|
|
57
|
+
- `news`
|
|
58
|
+
- `exchange`
|
|
59
|
+
- `fund`
|
|
60
|
+
- `search`
|
|
61
|
+
- `web`
|
|
62
|
+
- `polymarket`
|
|
63
|
+
- `kalshi`
|
|
64
|
+
- `prediction_market`
|
|
114
65
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
| Env Var | Default | Purpose |
|
|
118
|
-
|---------|---------|---------|
|
|
119
|
-
| `VITE_PORT` | `5173` | Frontend dev server port |
|
|
120
|
-
| `VITE_BACKEND_PORT` | `3001` | Backend port for Vite proxy target |
|
|
121
|
-
|
|
122
|
-
### How routing works
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
Sandbox (urania preview):
|
|
126
|
-
Frontend hook: useMarketPrice()
|
|
127
|
-
→ fetch /proxy/market/price (same-origin to Vite)
|
|
128
|
-
→ Vite proxy → Express /proxy/* → OutboundProxy (JWT) → hermod
|
|
129
|
-
|
|
130
|
-
Backend route: dataApi.market.price()
|
|
131
|
-
→ fetch SURF_SANDBOX_PROXY_BASE/market/price
|
|
132
|
-
→ OutboundProxy (JWT) → hermod
|
|
133
|
-
|
|
134
|
-
Deployed (surf.computer):
|
|
135
|
-
Frontend hook: useMarketPrice()
|
|
136
|
-
→ fetch /proxy/market/price (same-origin to Express)
|
|
137
|
-
→ Express /proxy/* → hermod (APP_TOKEN)
|
|
138
|
-
|
|
139
|
-
Backend route: dataApi.market.price()
|
|
140
|
-
→ fetch http://127.0.0.1:PORT/proxy/market/price (loopback)
|
|
141
|
-
→ Express /proxy/* → hermod (APP_TOKEN)
|
|
142
|
-
|
|
143
|
-
Public (local dev):
|
|
144
|
-
Frontend hook: useMarketPrice()
|
|
145
|
-
→ fetch /proxy/market/price (same-origin to Vite)
|
|
146
|
-
→ Vite proxy → Express /proxy/* → hermod (GATEWAY_URL + APP_TOKEN)
|
|
147
|
-
|
|
148
|
-
Backend route: dataApi.market.price()
|
|
149
|
-
→ fetch SURF_DEPLOYED_GATEWAY_URL/gateway/v1/market/price (Bearer APP_TOKEN)
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Available Categories
|
|
153
|
-
|
|
154
|
-
| Category | Example Methods |
|
|
155
|
-
|----------|----------------|
|
|
156
|
-
| `market` | `price`, `ranking`, `etf`, `futures`, `options`, `fear_greed` |
|
|
157
|
-
| `token` | `holders`, `transfers`, `dex_trades`, `tokenomics` |
|
|
158
|
-
| `wallet` | `detail`, `net_worth`, `labels_batch`, `transfers` |
|
|
159
|
-
| `onchain` | `sql`, `tx`, `gas_price`, `schema`, `structured_query` |
|
|
160
|
-
| `social` | `detail`, `mindshare`, `tweets`, `user`, `ranking` |
|
|
161
|
-
| `project` | `detail`, `defi_metrics`, `defi_ranking` |
|
|
162
|
-
| `news` | `detail`, `feed` |
|
|
163
|
-
| `exchange` | `price`, `depth`, `klines`, `funding_history`, `perp` |
|
|
164
|
-
| `fund` | `detail`, `portfolio`, `ranking` |
|
|
165
|
-
| `search` | `project`, `news`, `wallet`, `web` |
|
|
166
|
-
| `web` | `fetch` |
|
|
167
|
-
| `polymarket` | `events`, `markets`, `prices`, `volumes` |
|
|
168
|
-
| `kalshi` | `events`, `markets`, `prices`, `volumes` |
|
|
169
|
-
| `prediction_market` | `category_metrics` |
|
|
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`:
|
|
66
|
+
## Server runtime
|
|
178
67
|
|
|
179
68
|
```js
|
|
180
|
-
const {
|
|
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)
|
|
69
|
+
const { createServer } = require('@surf-ai/sdk/server')
|
|
229
70
|
|
|
230
|
-
|
|
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])
|
|
71
|
+
createServer({ port: 3001 }).start()
|
|
233
72
|
```
|
|
234
73
|
|
|
235
|
-
|
|
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` |
|
|
74
|
+
`createServer()` provides:
|
|
245
75
|
|
|
246
|
-
|
|
76
|
+
- Auto-loading of `routes/*.js` and `routes/*.ts` as `/api/{name}`
|
|
77
|
+
- `GET /api/health`
|
|
78
|
+
- `POST /api/__sync-schema`
|
|
79
|
+
- `GET/POST/PATCH/DELETE /api/cron`
|
|
80
|
+
- `POST /api/cron/:id/run`
|
|
81
|
+
- Schema sync on startup and when `db/schema.js` changes
|
|
247
82
|
|
|
248
|
-
|
|
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
|
|
83
|
+
`GET /api/health` is public.
|
|
253
84
|
|
|
254
|
-
|
|
85
|
+
These runtime endpoints require `Authorization: Bearer <SURF_API_KEY>`:
|
|
255
86
|
|
|
256
|
-
|
|
87
|
+
- `POST /api/__sync-schema`
|
|
88
|
+
- `GET /api/cron`
|
|
89
|
+
- `POST /api/cron`
|
|
90
|
+
- `PATCH /api/cron/:id`
|
|
91
|
+
- `DELETE /api/cron/:id`
|
|
92
|
+
- `POST /api/cron/:id/run`
|
|
257
93
|
|
|
258
|
-
|
|
94
|
+
Routes you define in `routes/*` stay public unless your app adds its own auth.
|
|
259
95
|
|
|
260
|
-
|
|
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/`:
|
|
96
|
+
Example route:
|
|
281
97
|
|
|
282
98
|
```js
|
|
283
|
-
|
|
99
|
+
const router = require('express').Router()
|
|
284
100
|
const { dataApi } = require('@surf-ai/sdk/server')
|
|
285
101
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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).
|
|
102
|
+
router.get('/', async (_req, res) => {
|
|
103
|
+
const data = await dataApi.market.price({ symbol: 'BTC', time_range: '1d' })
|
|
104
|
+
res.json(data)
|
|
105
|
+
})
|
|
325
106
|
|
|
326
|
-
|
|
107
|
+
module.exports = router
|
|
108
|
+
```
|
|
327
109
|
|
|
328
|
-
|
|
110
|
+
## Database helpers
|
|
329
111
|
|
|
330
112
|
```js
|
|
331
|
-
|
|
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'
|
|
113
|
+
const { dbProvision, dbQuery, dbTables, dbTableSchema, dbStatus } = require('@surf-ai/sdk/db')
|
|
344
114
|
|
|
345
|
-
|
|
346
|
-
const
|
|
115
|
+
await dbProvision()
|
|
116
|
+
const result = await dbQuery('SELECT * FROM users WHERE id = $1', [123], { arrayMode: true })
|
|
117
|
+
const tables = await dbTables()
|
|
118
|
+
const schema = await dbTableSchema('users')
|
|
119
|
+
const status = await dbStatus()
|
|
347
120
|
```
|
|
348
121
|
|
|
349
|
-
|
|
122
|
+
Define tables in `db/schema.js` and the runtime will provision the database and create missing tables and columns during startup.
|
|
350
123
|
|
|
351
|
-
|
|
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
|
|
124
|
+
Example schema:
|
|
375
125
|
|
|
376
126
|
```js
|
|
377
|
-
|
|
378
|
-
const { dataApi } = require('@surf-ai/sdk/server')
|
|
379
|
-
const router = require('express').Router()
|
|
127
|
+
const { pgTable, serial, text, timestamp } = require('drizzle-orm/pg-core')
|
|
380
128
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
dataApi.social.detail({ username: req.query.twitter }),
|
|
387
|
-
])
|
|
388
|
-
res.json({ price: price.data?.[0], topHolders: holders.data, social: social.data })
|
|
129
|
+
exports.users = pgTable('users', {
|
|
130
|
+
id: serial('id').primaryKey(),
|
|
131
|
+
name: text('name').notNull(),
|
|
132
|
+
email: text('email'),
|
|
133
|
+
created_at: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
|
389
134
|
})
|
|
390
|
-
|
|
391
|
-
module.exports = router
|
|
392
135
|
```
|
|
393
136
|
|
|
394
|
-
##
|
|
395
|
-
|
|
396
|
-
API methods and React hooks are auto-generated from hermod's OpenAPI spec via the surf CLI:
|
|
137
|
+
## 1.0 migration notes
|
|
397
138
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
python scripts/gen_sdk.py --ops market-price token-holders
|
|
404
|
-
|
|
405
|
-
# Build
|
|
406
|
-
bun run build
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
Requires `surf` CLI installed and authenticated (`surf login`).
|
|
410
|
-
|
|
411
|
-
## Development
|
|
412
|
-
|
|
413
|
-
```bash
|
|
414
|
-
bun install
|
|
415
|
-
bun run build # compile TypeScript
|
|
416
|
-
bun test # run tests
|
|
417
|
-
bun run codegen # regenerate from OpenAPI spec
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
## Testing
|
|
421
|
-
|
|
422
|
-
```bash
|
|
423
|
-
# Unit tests
|
|
424
|
-
bun test ./tests/data-client.test.ts
|
|
425
|
-
|
|
426
|
-
# E2E (auto-detects mode from env vars)
|
|
427
|
-
bun test ./tests/e2e-all-envs.test.ts
|
|
428
|
-
|
|
429
|
-
# E2E with specific mode:
|
|
430
|
-
SURF_SANDBOX_PROXY_BASE=http://127.0.0.1:9999/s/<session>/proxy bun test ./tests/e2e-all-envs.test.ts
|
|
431
|
-
SURF_DEPLOYED_GATEWAY_URL=https://api.ask.surf SURF_DEPLOYED_APP_TOKEN=<token> bun test ./tests/e2e-all-envs.test.ts
|
|
432
|
-
```
|
|
139
|
+
- `SURF_API_BASE_URL` and `SURF_API_KEY` are the only supported SDK auth variables.
|
|
140
|
+
- The runtime no longer mounts `/proxy/*`.
|
|
141
|
+
- The `@surf-ai/sdk/react` subpath has been removed.
|
|
142
|
+
- Route modules must export the handler directly with `module.exports = router`.
|
|
143
|
+
- `createServer()` requires a port from `options.port` or `process.env.PORT`.
|