@orkid-labs/sdk 0.1.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 ADDED
@@ -0,0 +1,689 @@
1
+ # Orkid Swap API — Integration Kit
2
+
3
+ This kit contains everything an external integrator needs to swap through the Orkid solver using the two public API endpoints:
4
+
5
+ - `POST /api/v1/route` — get an executable quote
6
+ - `POST /api/v1/solve` — sign a Permit2 message and execute gaslessly
7
+
8
+ It includes a TypeScript SDK, request/response types, signing helpers for **viem** and **ethers**, copy-paste examples, an OpenAPI spec, and a step-by-step integration guide.
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ 1. [What Orkid gives an integrator](#what-orkid-gives-an-integrator)
15
+ 2. [Supported chains](#supported-chains)
16
+ 3. [Environments: production vs sandbox](#environments-production-vs-sandbox)
17
+ 4. [API key & rate limits](#api-key--rate-limits)
18
+ 5. [High-level flow](#high-level-flow)
19
+ 6. [Endpoint 1: /api/v1/route](#endpoint-1-apiv1route)
20
+ 7. [Endpoint 2: /api/v1/solve](#endpoint-2-apiv1solve)
21
+ 8. [Permit2 signing walkthrough](#permit2-signing-walkthrough)
22
+ 9. [Error & retry guide](#error--retry-guide)
23
+ 10. [SDK reference](#sdk-reference)
24
+ 11. [CLI](#cli)
25
+ 12. [Reporting endpoints](#reporting-endpoints)
26
+ 13. [Examples](#examples)
27
+ 14. [OpenAPI](#openapi)
28
+
29
+ ---
30
+
31
+ ## What Orkid gives an integrator
32
+
33
+ - **Same-chain, gasless swaps** on Base, Ethereum, Arbitrum, and Polygon.
34
+ - **Competitive Orkid fee (contact us for pricing)** taken from the input token.
35
+ - **Bundled / gasless execution** — the Orkid solver submits the transaction and pays the gas.
36
+ - **MEV-aware routing** using Tycho's live pool graph and simulation. On Ethereum, transactions are broadcast through Flashbots Protect (private mempool) to protect against sandwich attacks.
37
+ - **Per-account volume & rebate tracking** in the Orkid board admin.
38
+ - **Non-custodial** — the solver uses Permit2 to pull the input token and the net output goes straight to the user.
39
+ - **Sandbox environment** — a dedicated dry-run endpoint you can hammer without touching real flows.
40
+
41
+ ---
42
+
43
+ ## Supported chains
44
+
45
+ | Chain | ID | Status | TVMExecutor (spender) | Min notional |
46
+ | --- | --- | --- | --- | --- |
47
+ | Base | 8453 | **Live** | `0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289` | $20 |
48
+ | Ethereum | 1 | **Live** | `0xCfc33b521190FcD414c8f81f479749c4dCE8f69b` | $200 |
49
+ | Arbitrum | 42161 | **Live** | `0xf57235609bf99fb0b017e95b3bc33a606a541374` | $50 |
50
+ | Polygon | 137 | **Live** | `0xd633Ea84E6E2Db002C14C6fe3A064eE2cc4E258c` | $50 |
51
+
52
+ All four chains use the canonical Permit2 contract: `0x000000000022D473030F116dDEE9F6B43aC78BA3`.
53
+
54
+ The `spender` field in your signed Permit2 message must match the TVMExecutor address for the target chain. The SDK handles this automatically — see [Permit2 signing walkthrough](#permit2-signing-walkthrough) for manual integrations.
55
+
56
+ ---
57
+
58
+ ## Environments: production vs sandbox
59
+
60
+ Orkid provides two environments. Both use the same API paths (`/api/v1/route`, `/api/v1/solve`, etc.) — the **base URL** determines which environment you hit.
61
+
62
+ | Environment | Base URL | Solve behavior | Volume tracked |
63
+ | --- | --- | --- | --- |
64
+ | **Production** | `https://orkidlabs.xyz` | Real execution (submits on-chain tx) | Yes — counts toward rebates |
65
+ | **Sandbox** | `https://sandbox.orkidlabs.com` | **Dry-run only** (never submits) | No — sandbox traffic is isolated |
66
+
67
+ ### Sandbox
68
+
69
+ The sandbox is a dedicated environment for integration testing. It:
70
+
71
+ - Forwards `/api/v1/route` to the **real solvers** — you get authentic quotes, routing, and latency.
72
+ - Forces `/api/v1/solve` to `dryRun: true` **regardless of what you send** — the solver encodes and validates the transaction but never submits it.
73
+ - Uses **separate API keys** from production.
74
+ - Does **not** record usage or rebate volume.
75
+ - Has its own rate limit (300 req/min for the pilot key).
76
+
77
+ This means you can hammer the sandbox with thousands of test requests, try different token pairs and amounts, validate your Permit2 signing flow end-to-end, and never risk a real on-chain transaction or pollute your production volume accounting.
78
+
79
+ ### Getting a sandbox key
80
+
81
+ Contact Orkid for a sandbox API key. Sandbox keys are 40-character hex strings, separate from production keys:
82
+
83
+ ```text
84
+ X-ORKID-API-Key: <sandbox-key>
85
+ ```
86
+
87
+ ### Using the sandbox with the SDK
88
+
89
+ ```typescript
90
+ import { OrkidClient, SANDBOX_BASE_URL } from '@orkid/sdk'
91
+
92
+ const sandbox = new OrkidClient({
93
+ apiKey: process.env.ORKID_SANDBOX_KEY!,
94
+ baseUrl: SANDBOX_BASE_URL, // https://sandbox.orkidlabs.com
95
+ })
96
+
97
+ // Real quote from the real solver
98
+ const quote = await sandbox.getQuote({
99
+ from: 'USDC',
100
+ to: 'WETH',
101
+ amount: '25.0',
102
+ chain: 'base',
103
+ })
104
+
105
+ // Dry-run solve — encodes and validates but never submits
106
+ const result = await sandbox.solve({
107
+ from: 'USDC',
108
+ to: 'WETH',
109
+ amount: '25.0',
110
+ chain: 'base',
111
+ user: '0x...',
112
+ fromAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
113
+ fromDecimals: 6,
114
+ toAddress: '0x4200000000000000000000000000000000000006',
115
+ toDecimals: 18,
116
+ dryRun: false, // ignored — sandbox always forces dryRun: true
117
+ permit: signed.permit,
118
+ signature: signed.signature,
119
+ })
120
+ // result.transaction.to / .data are real calldata
121
+ // result.transaction.txHash is absent (never submitted)
122
+ ```
123
+
124
+ ### Using the sandbox with curl
125
+
126
+ ```bash
127
+ # Health check (no auth required)
128
+ curl https://sandbox.orkidlabs.com/health
129
+
130
+ # Route (real quote)
131
+ curl -X POST https://sandbox.orkidlabs.com/api/v1/route \
132
+ -H "X-ORKID-API-Key: <sandbox-key>" \
133
+ -H "Content-Type: application/json" \
134
+ -d '{"from":"USDC","to":"WETH","amount":"25.0","chain":"base","fromAddress":"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913","fromDecimals":6,"toAddress":"0x4200000000000000000000000000000000000006","toDecimals":18}'
135
+
136
+ # Solve (always dry-run — never submits)
137
+ curl -X POST https://sandbox.orkidlabs.com/api/v1/solve \
138
+ -H "X-ORKID-API-Key: <sandbox-key>" \
139
+ -H "Content-Type: application/json" \
140
+ -d '{"from":"USDC","to":"WETH","amount":"25.0","chain":"base","user":"0x...","fromAddress":"0x...","fromDecimals":6,"toAddress":"0x...","toDecimals":18,"dryRun":false,"permit":{...},"signature":"0x..."}'
141
+ ```
142
+
143
+ ### Sandbox safety guarantees
144
+
145
+ - **No on-chain execution**: the sandbox proxy intercepts every `/api/v1/solve` and forces `dryRun: true` before forwarding to the solver. Even if you send `dryRun: false`, the sandbox ignores it.
146
+ - **No production volume**: sandbox requests do not call the production usage-tracking or rebate-recording endpoints.
147
+ - **Separate keys**: sandbox API keys are distinct from production keys. A production key will not work on the sandbox and vice versa.
148
+ - **Real routing**: `/api/v1/route` hits the actual per-chain solvers, so quotes reflect live pool state and real latency.
149
+
150
+ ---
151
+
152
+ ## API key & rate limits
153
+
154
+ Every programmatic request must include:
155
+
156
+ ```text
157
+ X-ORKID-API-Key: <40-char-hex-key>
158
+ ```
159
+
160
+ API keys are 40-character hex strings. Only a SHA-256 hash is stored on Orkid's side, so the plaintext key is shown once when it is created.
161
+
162
+ To create or rotate a key, log in to the Orkid board admin (`/board/admin`) with a master account, open the API Accounts section, and generate a key for the partner account.
163
+
164
+ Default rate limits:
165
+
166
+ | Environment | Tier | Requests / minute |
167
+ | --- | --- | --- |
168
+ | Production | free | 60 |
169
+ | Production | pro | 120 |
170
+ | Production | enterprise | 1000 |
171
+ | Sandbox | pilot | 300 |
172
+
173
+ ---
174
+
175
+ ## High-level flow
176
+
177
+ 1. **Get a quote** (`/api/v1/route`)
178
+ 2. **Approve the input token to Permit2** (one-time per token)
179
+ 3. **Find an unused Permit2 nonce** by reading the nonce bitmap on-chain
180
+ 4. **Sign a Permit2 `PermitTransferFrom` message** with the TVMExecutor as `spender`
181
+ 5. **Call `/api/v1/solve`** with `dryRun: false` to submit gaslessly
182
+ 6. Optionally fall back to `dryRun: true` and have the user send the transaction themselves if the solver cannot subsidize gas
183
+
184
+ > **Sandbox tip:** Steps 1-5 work identically in the sandbox. Step 5 always behaves as `dryRun: true` — you get back encoded calldata but no transaction is submitted. This is the perfect way to validate your signing flow before going live.
185
+
186
+ ---
187
+
188
+ ## Endpoint 1: `/api/v1/route`
189
+
190
+ ### Request
191
+
192
+ ```json
193
+ POST https://orkidlabs.xyz/api/v1/route
194
+ Content-Type: application/json
195
+ X-ORKID-API-Key: <key>
196
+
197
+ {
198
+ "from": "USDC",
199
+ "to": "WETH",
200
+ "amount": "25.0",
201
+ "chain": "base",
202
+ "fromAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
203
+ "fromDecimals": 6,
204
+ "toAddress": "0x4200000000000000000000000000000000000006",
205
+ "toDecimals": 18
206
+ }
207
+ ```
208
+
209
+ `from` and `to` can be token symbols (e.g. `USDC`, `WETH`). Explicit `*_address` + `*_decimals` is recommended because symbol resolution is server-side and may not cover every token.
210
+
211
+ `chain` accepts: `base`, `ethereum`, `arbitrum`, `polygon`.
212
+
213
+ ### Response
214
+
215
+ ```json
216
+ {
217
+ "ok": true,
218
+ "quote": {
219
+ "amountIn": "25.000000",
220
+ "amountOut": "0.010184",
221
+ "amountOutRaw": "10183963710975259",
222
+ "volumeUsd": 25,
223
+ "rate": "0.000407 WETH/USDC",
224
+ "protocol": "aerodrome_slipstreams",
225
+ "poolAddress": "0xc758d81b9b81a6fcdad075bd471874a2c46b54e0",
226
+ "priceImpactBps": 9.66
227
+ },
228
+ "gaslessEligible": true,
229
+ "computeMs": 142
230
+ }
231
+ ```
232
+
233
+ ### Error response
234
+
235
+ ```json
236
+ {
237
+ "ok": false,
238
+ "error": "Swap too small for gasless execution: $5.00 below $20 minimum on base.",
239
+ "gaslessEligible": false,
240
+ "computeMs": 45
241
+ }
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Endpoint 2: `/api/v1/solve`
247
+
248
+ ### Request Shape
249
+
250
+ ```json
251
+ POST https://orkidlabs.xyz/api/v1/solve
252
+ Content-Type: application/json
253
+ X-ORKID-API-Key: <key>
254
+
255
+ {
256
+ "from": "USDC",
257
+ "to": "WETH",
258
+ "amount": "25.0",
259
+ "chain": "base",
260
+ "user": "0x...",
261
+ "fromAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
262
+ "fromDecimals": 6,
263
+ "toAddress": "0x4200000000000000000000000000000000000006",
264
+ "toDecimals": 18,
265
+ "dryRun": false,
266
+ "slippageBps": 5,
267
+ "permit": {
268
+ "permitted": {
269
+ "token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
270
+ "amount": "25000000"
271
+ },
272
+ "nonce": "123...",
273
+ "deadline": "1725561600"
274
+ },
275
+ "signature": "0x..."
276
+ }
277
+ ```
278
+
279
+ ### Success response
280
+
281
+ ```json
282
+ {
283
+ "ok": true,
284
+ "quote": {
285
+ "amountIn": "25.000000",
286
+ "amountOut": "0.010184",
287
+ "amountOutRaw": "10183963710975259",
288
+ "volumeUsd": 25,
289
+ "rate": "0.000407 WETH/USDC",
290
+ "protocol": "aerodrome_slipstreams",
291
+ "poolAddress": "0xc758d81b9b81a6fcdad075bd471874a2c46b54e0",
292
+ "priceImpactBps": 9.66
293
+ },
294
+ "transaction": {
295
+ "txHash": "0x...",
296
+ "to": "0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289",
297
+ "data": "0x...",
298
+ "value": "0",
299
+ "chain": "base"
300
+ },
301
+ "computeMs": 142
302
+ }
303
+ ```
304
+
305
+ ### `dryRun: true` response
306
+
307
+ When `dryRun` is `true`, the solver does **not** submit the transaction. It returns the same `transaction` object minus `txHash`. The integrator can then ask the user to send the transaction themselves:
308
+
309
+ ```json
310
+ {
311
+ "ok": true,
312
+ "transaction": {
313
+ "to": "0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289",
314
+ "data": "0x...",
315
+ "value": "0",
316
+ "chain": "base"
317
+ }
318
+ }
319
+ ```
320
+
321
+ > **In the sandbox**, `/api/v1/solve` always returns this dry-run shape regardless of the `dryRun` value you send.
322
+
323
+ ---
324
+
325
+ ## Permit2 signing walkthrough
326
+
327
+ Permit2 lets a user approve a token transfer with an off-chain signature. The `PermitTransferFrom` signature authorizes a specific `spender` (the TVMExecutor) to pull an exact amount of a specific token before a deadline, using a specific one-time nonce.
328
+
329
+ ### Step 1 — Approve the token to Permit2 (one-time)
330
+
331
+ Before the first swap of each token, the user must call:
332
+
333
+ ```solidity
334
+ IERC20(token).approve(PERMIT2, type(uint256).max)
335
+ ```
336
+
337
+ ### Step 2 — Get the correct TVMExecutor address
338
+
339
+ The `spender` in the signed permit must match the TVMExecutor on the target chain:
340
+
341
+ | Chain | TVMExecutor |
342
+ | --- | --- |
343
+ | Base | `0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289` |
344
+ | Ethereum | `0xCfc33b521190FcD414c8f81f479749c4dCE8f69b` |
345
+ | Arbitrum | `0xf57235609bf99fb0b017e95b3bc33a606a541374` |
346
+ | Polygon | `0xd633Ea84E6E2Db002C14C6fe3A064eE2cc4E258c` |
347
+
348
+ ### Step 3 — Find an unused nonce
349
+
350
+ Permit2 stores a nonce bitmap for each user. If you reuse a nonce, the transaction reverts.
351
+
352
+ The nonce layout is:
353
+
354
+ ```text
355
+ wordPos = uint248(nonce >> 8)
356
+ bitPos = uint8(nonce)
357
+ ```
358
+
359
+ Read `nonceBitmap(owner, wordPos)` from the Permit2 contract until you find a word with an unset bit, then construct `(wordPos << 8) | bitPos`.
360
+
361
+ ### Step 4 — Sign the Permit2 typed data
362
+
363
+ Domain:
364
+
365
+ ```json
366
+ {
367
+ "name": "Permit2",
368
+ "chainId": 8453,
369
+ "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
370
+ }
371
+ ```
372
+
373
+ Types:
374
+
375
+ ```json
376
+ {
377
+ "PermitTransferFrom": [
378
+ { "name": "permitted", "type": "TokenPermissions" },
379
+ { "name": "spender", "type": "address" },
380
+ { "name": "nonce", "type": "uint256" },
381
+ { "name": "deadline", "type": "uint256" }
382
+ ],
383
+ "TokenPermissions": [
384
+ { "name": "token", "type": "address" },
385
+ { "name": "amount", "type": "uint256" }
386
+ ]
387
+ }
388
+ ```
389
+
390
+ Message:
391
+
392
+ ```json
393
+ {
394
+ "permitted": {
395
+ "token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
396
+ "amount": "25000000"
397
+ },
398
+ "spender": "0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289",
399
+ "nonce": "123456...",
400
+ "deadline": "1725561600"
401
+ }
402
+ ```
403
+
404
+ The `spender` must be in the signed message but is **not** included in the JSON `permit` object sent to `/solve`.
405
+
406
+ ### Step 5 — Call `/api/v1/solve`
407
+
408
+ Send the `permit` (without `spender`) and the 65-byte hex `signature`.
409
+
410
+ ---
411
+
412
+ ## Error & retry guide
413
+
414
+ | HTTP / `ok` | Likely cause | Recommended action |
415
+ | --- | --- | --- |
416
+ | `400` / `ok:false` | Invalid request shape | Fix the JSON, check `fromDecimals`, `toDecimals`, `chain` |
417
+ | `401` / `Invalid or revoked API key` | Bad/missing key or rate limit | Check `X-ORKID-API-Key` header, request a key rotation |
418
+ | `403` / `Missing anti-scraping token` | Browser request without page token | Use an API key instead |
419
+ | `200` / `error: "Swap too small..."` | Below gasless minimum | Increase amount or set `dryRun: true` and have user pay gas |
420
+ | `200` / `error: "Gas estimation failed"` | Permit/signature/calldata will revert | Check token approval, nonce, spender, chain, token addresses, balance |
421
+ | `200` / `error: "Invalid nonce"` | Nonce already used | Call `findUnusedNonce` again |
422
+ | `200` / `error: "TRANSFER_FROM_FAILED"` | Insufficient allowance or balance | Check `token.approve(PERMIT2, ...)` and `balanceOf` |
423
+ | `200` / `error: "Price impact too high..."` | Slippage exceeds max (500 bps) | Try a smaller amount or a different token pair |
424
+
425
+ ### Retry rules
426
+
427
+ - Retry `5xx` up to 3 times with exponential backoff.
428
+ - Do not blindly retry a successful `/solve` (`ok: true`) — it may have already submitted.
429
+ - For `dryRun: false`, if the solver returns `ok: true` with `transaction.txHash`, the swap is submitted.
430
+ - For `dryRun: true`, cache the returned `data` and submit it with `sendTransaction` only once.
431
+ - In the sandbox, retries are safe — no transaction is ever submitted.
432
+
433
+ ---
434
+
435
+ ## SDK reference
436
+
437
+ ### Install
438
+
439
+ ```bash
440
+ npm install @orkid/sdk
441
+ # or copy the source files into your project
442
+ ```
443
+
444
+ Peer dependencies (only needed for signing):
445
+
446
+ ```bash
447
+ npm install viem
448
+ # or
449
+ npm install ethers
450
+ ```
451
+
452
+ ### Quick start — production
453
+
454
+ ```typescript
455
+ import { OrkidClient, OrkidPermitSigner } from '@orkid/sdk'
456
+ import { createWalletClient, http } from 'viem'
457
+ import { base } from 'viem/chains'
458
+ import { privateKeyToAccount } from 'viem/accounts'
459
+
460
+ const client = new OrkidClient({
461
+ apiKey: process.env.ORKID_API_KEY!,
462
+ // baseUrl defaults to https://orkidlabs.xyz
463
+ })
464
+
465
+ const account = privateKeyToAccount('0x...')
466
+ const walletClient = createWalletClient({ account, chain: base, transport: http() })
467
+
468
+ const permitSigner = new OrkidPermitSigner(walletClient)
469
+
470
+ async function swapUsdcToWeth() {
471
+ const quote = await client.getQuote({
472
+ from: 'USDC',
473
+ to: 'WETH',
474
+ amount: '25.0',
475
+ chain: 'base',
476
+ })
477
+
478
+ if (!quote.ok) throw new Error(quote.error)
479
+
480
+ const signed = await permitSigner.prepareAndSign({
481
+ fromToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
482
+ fromDecimals: 6,
483
+ amount: '25.0',
484
+ chain: 'base',
485
+ })
486
+
487
+ const result = await client.solve({
488
+ from: 'USDC',
489
+ to: 'WETH',
490
+ amount: '25.0',
491
+ chain: 'base',
492
+ user: account.address,
493
+ fromAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
494
+ fromDecimals: 6,
495
+ toAddress: '0x4200000000000000000000000000000000000006',
496
+ toDecimals: 18,
497
+ dryRun: false,
498
+ permit: signed.permit,
499
+ signature: signed.signature,
500
+ })
501
+
502
+ if (!result.ok) throw new Error(result.error)
503
+ console.log('txHash', result.transaction?.txHash)
504
+ }
505
+ ```
506
+
507
+ ### Quick start — sandbox
508
+
509
+ ```typescript
510
+ import { OrkidClient, OrkidPermitSigner, SANDBOX_BASE_URL } from '@orkid/sdk'
511
+
512
+ const sandbox = new OrkidClient({
513
+ apiKey: process.env.ORKID_SANDBOX_KEY!,
514
+ baseUrl: SANDBOX_BASE_URL, // https://sandbox.orkidlabs.com
515
+ })
516
+
517
+ // Same API surface — quotes are real, solves are always dry-run
518
+ const quote = await sandbox.getQuote({
519
+ from: 'USDC',
520
+ to: 'WETH',
521
+ amount: '25.0',
522
+ chain: 'arbitrum',
523
+ })
524
+ ```
525
+
526
+ ### Exports
527
+
528
+ | Export | Description |
529
+ | --- | --- |
530
+ | `OrkidClient` | Main API client — `getQuote`, `solve`, `confirmTransaction`, `getAccount`, `getUsage`, `getRebates` |
531
+ | `SANDBOX_BASE_URL` | `'https://sandbox.orkidlabs.com'` — pass as `baseUrl` for the sandbox |
532
+ | `OrkidPermitSigner` | Permit2 signing helper (viem and ethers variants) |
533
+ | `ORKID_CHAIN_CONFIG` | Per-chain metadata (TVMExecutor, Permit2, RPC, explorer, min notional) |
534
+ | `PERMIT2` | Canonical Permit2 address: `0x000000000022D473030F116dDEE9F6B43aC78BA3` |
535
+
536
+ ---
537
+
538
+ ## CLI
539
+
540
+ The package ships an `orkid` binary for testing integration without writing code:
541
+
542
+ ```bash
543
+ # via npx once published, or `node dist/cli.js` inside this repo
544
+ export ORKID_API_KEY=<your-40-hex-key>
545
+ # export ORKID_API_URL=https://orkidlabs.xyz # optional override
546
+
547
+ orkid quote --from USDC --to WETH --amount 25 --chain base
548
+ orkid tokens --chain base --search USDC
549
+ orkid account # partner account info + rebate rate
550
+ orkid usage # your account's usage events
551
+ orkid usage --event-type solve --period 2026-09
552
+ orkid rebates # your accrued monthly rebates
553
+ orkid rebates --period 2026-09
554
+ orkid status # API health check
555
+ ```
556
+
557
+ Global flags: `--api-key`, `--base-url`, `--json` (raw JSON output), `-h/--help`.
558
+
559
+ To target the sandbox, pass `--base-url https://sandbox.orkidlabs.com`:
560
+
561
+ ```bash
562
+ orkid --base-url https://sandbox.orkidlabs.com --api-key <sandbox-key> status
563
+ orkid --base-url https://sandbox.orkidlabs.com --api-key <sandbox-key> quote --from USDC --to WETH --amount 25 --chain base
564
+ ```
565
+
566
+ Live solve requires a funded wallet with a Permit2 allowance and `viem` installed:
567
+
568
+ ```bash
569
+ npm i viem
570
+ export ORKID_PRIVATE_KEY=0x...
571
+ orkid solve --from USDC --to WETH --amount 25 \
572
+ --from-address 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 --from-decimals 6 \
573
+ --to-address 0x4200000000000000000000000000000000000006 --to-decimals 18
574
+ # default is a dry-run — pass --execute to submit the live swap
575
+ ```
576
+
577
+ ---
578
+
579
+ ## Reporting endpoints
580
+
581
+ Partners can query their own account, usage, and rebate data with the same `X-ORKID-API-Key` header. All responses are scoped to the account that owns the key — a key can never see another partner's data.
582
+
583
+ > **Sandbox note:** These endpoints return empty/zero results on the sandbox because sandbox traffic is not tracked.
584
+
585
+ ### `GET /api/v1/account`
586
+
587
+ Returns the partner account, including the configured rebate rate.
588
+
589
+ ```bash
590
+ curl -H "X-ORKID-API-Key: <key>" https://orkidlabs.xyz/api/v1/account
591
+ ```
592
+
593
+ ```json
594
+ { "ok": true, "account": { "id": "…", "slug": "alternatefutures", "name": "Alternate Futures", "rebate_bps": 3, "rebate_active": true } }
595
+ ```
596
+
597
+ ### `GET /api/v1/usage`
598
+
599
+ Lists usage events (quotes and solves) recorded for the account. Optional filters: `?period=YYYY-MM`, `?event_type=route|solve`, `?limit=N` (default 100).
600
+
601
+ ```bash
602
+ curl -H "X-ORKID-API-Key: <key>" "https://orkidlabs.xyz/api/v1/usage?event_type=solve&period=2026-09"
603
+ ```
604
+
605
+ ```json
606
+ { "ok": true, "events": [ { "event_type": "solve", "volume_usd": 25, "is_dry_run": true, "created_at": "…" } ], "total_volume_usd": 25 }
607
+ ```
608
+
609
+ ### `GET /api/v1/rebates`
610
+
611
+ Lists monthly rebate ledger entries. Optional filter: `?period=YYYY-MM`.
612
+
613
+ ```bash
614
+ curl -H "X-ORKID-API-Key: <key>" "https://orkidlabs.xyz/api/v1/rebates?period=2026-09"
615
+ ```
616
+
617
+ ```json
618
+ { "ok": true, "rebates": [ { "period": "monthly", "period_start": "2026-09-01T00:00:00+00:00", "volume_usd": 100, "rebate_bps": 3, "rebate_usd": 0.03, "status": "accrued" } ] }
619
+ ```
620
+
621
+ Rebate volume counts only **non-dry-run, non-test `solve` events** — quote requests, dry runs, and operator test traffic (`is_test: true`) are excluded. `rebate_usd = volume_usd × rebate_bps / 10,000`. Entries move from `accrued` → `paid` when Orkid settles monthly.
622
+
623
+ ### Referral rebates
624
+
625
+ Partners who source other partners earn a referral cut of the referred account's volume, on top of their own. Referral income appears in `GET /api/v1/rebates` as separate ledger rows:
626
+
627
+ ```json
628
+ { "kind": "referral", "source_account_id": "<referred-account-uuid>", "volume_usd": 1000, "rebate_bps": 1, "rebate_usd": 0.10 }
629
+ ```
630
+
631
+ `kind: "volume"` rows are your own volume; `kind: "referral"` rows pay you for a referred partner's volume (`source_account_id` identifies which one). Example split: a referred partner at 2 bps + 1 bps referral keeps total cost at 3 bps — the sourcer earns from their network's activity.
632
+
633
+ ### `GET /api/v1/usage` — event flags
634
+
635
+ Each event carries `is_dry_run` (encode-only, not executed) and `is_test` (operator-marked test traffic). Only `is_dry_run=false, is_test=false` solve events count toward rebates.
636
+
637
+ ### User-pays-gas swaps (below the gasless minimum)
638
+
639
+ Quotes and solves below the gasless floor still work — the response carries `gaslessEligible: false` and `/api/v1/solve` returns encode-only calldata. The client submits the transaction itself (the user's wallet pays gas).
640
+
641
+ To count that settled volume toward rebates, the client **must call `POST /api/v1/confirm`** once the tx is mined:
642
+
643
+ ```bash
644
+ curl -X POST https://orkidlabs.xyz/api/v1/confirm \
645
+ -H "X-ORKID-API-Key: <key>" -H "Content-Type: application/json" \
646
+ -d '{"txHash": "0x…", "chain": "base"}'
647
+ ```
648
+
649
+ The API verifies on-chain that the transaction succeeded, targeted the TVMExecutor, and matches the exact calldata it encoded (`keccak256` of `tx.input`). Self-reported hashes that don't match an encoded swap are rejected. The SDK (`client.confirmTransaction`), CLI (`orkid confirm <txHash>`, automatic after `solve --execute` fallbacks), and widget (automatic) call this for you — hand-rolled integrations must call it explicitly or below-min volume won't be tracked.
650
+
651
+ ---
652
+
653
+ ## Examples
654
+
655
+ See the `examples/` directory for complete, runnable examples:
656
+
657
+ - `examples/vanilla-viem.ts` — full flow with a viem wallet client
658
+ - `examples/vanilla-ethers.ts` — full flow with an ethers signer
659
+ - `examples/react.tsx` — React hook wrapper
660
+ - `examples/nextjs-server-action.ts` — server-side key protection in a Next.js app
661
+ - `examples/curl.sh` — copy-paste curl commands
662
+
663
+ ---
664
+
665
+ ## OpenAPI
666
+
667
+ The full OpenAPI 3.0 spec is in `openapi.yaml` and can be imported into Postman, Swagger UI, or used to generate client SDKs.
668
+
669
+ ---
670
+
671
+ ## Minimum notional by chain
672
+
673
+ | Chain | Minimum USD | Notes |
674
+ | --- | --- | --- |
675
+ | Base | $20 | Low L1 data fee, cheap gas |
676
+ | Ethereum | $200 | MEV-protected via Flashbots Protect |
677
+ | Arbitrum | $50 | Low fees, fast settlement |
678
+ | Polygon | $50 | Low fees, PoS |
679
+
680
+ Below the minimum, swaps still work but the user pays gas (the solver returns encode-only calldata via `dryRun` semantics — see [User-pays-gas swaps](#user-pays-gas-swaps-below-the-gasless-minimum)).
681
+
682
+ ---
683
+
684
+ ## Contact
685
+
686
+ For an API key, sandbox access, higher rate limits, or custom fee arrangements:
687
+
688
+ - Email: `api@orkidlabs.com`
689
+ - Dashboard: `https://orkidlabs.xyz/board/admin` (master accounts only)