@shake-defi/node 1.0.0 → 1.0.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.
Files changed (2) hide show
  1. package/README.md +290 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # @shake-defi/node
2
+
3
+ Node.js SDK for the **Token Purchase Platform API** — sell tokens for stablecoins without your customers touching wallets, gas, or smart contracts directly.
4
+
5
+ Modeled on Stripe's Payment Intents API: your backend creates a `TokenPurchaseIntent`, a hosted checkout widget confirms it on-chain, and a webhook tells you what actually happened.
6
+
7
+ ```js
8
+ const { PlatformClient } = require('@shake-defi/node');
9
+ const client = new PlatformClient(process.env.PLATFORM_SECRET_KEY);
10
+
11
+ const customer = await client.customers.create({ external_customer_id: 'user_88210' });
12
+
13
+ const intent = await client.tokenPurchaseIntents.create({
14
+ customer: customer.id,
15
+ amount: 5.00,
16
+ payment_currency: 'USDC',
17
+ });
18
+
19
+ // Send intent._widget_params (or intent.client_secret) to your frontend
20
+ // to initialize the checkout widget.
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @shake-defi/node
27
+ ```
28
+
29
+ Requires Node.js 18 or later (uses the built-in `fetch` and `AbortSignal.timeout`).
30
+
31
+ ## Authentication
32
+
33
+ Every request from your backend is authenticated with a secret key:
34
+
35
+ ```js
36
+ const client = new PlatformClient('sk_live_51Hxyz...');
37
+ ```
38
+
39
+ | Key prefix | Used where | Can do |
40
+ |---|---|---|
41
+ | `sk_test_` / `sk_live_` | Your backend (this SDK) | Create and retrieve resources |
42
+ | `pk_test_` / `pk_live_` | Your frontend (checkout widget) | Initialize the widget only |
43
+
44
+ `PlatformClient` throws synchronously if the key doesn't start with `sk_live_` or `sk_test_` — **never pass a publishable key (`pk_…`) to this SDK.**
45
+
46
+ ### Test vs. live mode
47
+
48
+ Which mode you're in is determined entirely by the key you construct the client with — there's no separate network parameter to set. Test mode runs on **Base Sepolia** with test stablecoins; live mode runs on **Base mainnet** with real ones. Test and live objects (customers, intents) are fully isolated from each other. Every object returned by the API carries a `livemode` boolean so you can confirm which mode produced it.
49
+
50
+ ## Client options
51
+
52
+ ```js
53
+ const client = new PlatformClient(secretKey, {
54
+ baseUrl: 'https://api.shake-defi.com', // override for a proxy or mock server
55
+ timeout: 30_000, // per-request timeout, ms
56
+ maxRetries: 2, // retries on network errors / 5xx only
57
+ });
58
+ ```
59
+
60
+ Requests are retried with exponential back-off (`200ms, 400ms, ...`) on network failures and `5xx` responses. `4xx` responses are never retried.
61
+
62
+ ## Customers
63
+
64
+ One record per payer. Create it once, the first time a customer interacts with the API, and store the returned `id` in your own database.
65
+
66
+ ```js
67
+ const customer = await client.customers.create({
68
+ external_customer_id: 'user_88210', // your own internal ID for this user
69
+ });
70
+ // => { id: 'cus_1JX9k2', object: 'customer', external_customer_id: 'user_88210', created: ..., livemode: true }
71
+
72
+ const same = await client.customers.retrieve('cus_1JX9k2');
73
+ ```
74
+
75
+ Creating a customer with an `external_customer_id` that already exists for your account and mode returns the existing customer rather than creating a duplicate — the call is safe to retry as-is, or you can also pass an idempotency key:
76
+
77
+ ```js
78
+ await client.customers.create(
79
+ { external_customer_id: 'user_88210' },
80
+ { idempotencyKey: 'user_88210_signup' }
81
+ );
82
+ ```
83
+
84
+ ## Token Purchase Intents
85
+
86
+ The core resource — a single attempt to buy tokens with a stablecoin.
87
+
88
+ ### Create an intent
89
+
90
+ ```js
91
+ const intent = await client.tokenPurchaseIntents.create({
92
+ customer: 'cus_1JX9k2',
93
+ amount: 5.00, // human-readable units — 5.00 means $5.00 equivalent, NOT smallest units
94
+ payment_currency: 'USDC', // must be a key in GET /account's currency_contracts
95
+ }, {
96
+ idempotencyKey: 'order_8841_attempt_1', // recommended: {order_id}_attempt_{n}
97
+ });
98
+ ```
99
+
100
+ The intent is created in `pending` status. **The creation response is only ever "accepted" — final status (`succeeded`, `failed`, `canceled`) arrives via [webhook](#webhooks), not in this response.** It expires to `canceled` if left unconfirmed for 30 minutes.
101
+
102
+ ### Initializing the checkout widget
103
+
104
+ The create response includes everything a frontend checkout widget needs — pass **one** of the following two fields to your widget SDK:
105
+
106
+ - **`intent._widget_params`** — a snapshot taken at creation time. No further API calls required from the frontend.
107
+ - **`intent.client_secret`** — a bearer credential (`{intent_id}_secret_{random}`) the widget exchanges for live params — including the current on-chain price — via the public `GET /v1/widget/params?client_secret=...` endpoint. This is safe to send to the browser by design, but treat it like any other credential: don't log it, and don't let one customer see another's.
108
+
109
+ ```js
110
+ res.json({
111
+ clientSecret: intent.client_secret, // hand this to your frontend
112
+ });
113
+ ```
114
+
115
+ `client_secret` is returned **only** on creation — it is never present on retrieve or list responses and can't be recovered afterward, since only its hash is persisted server-side.
116
+
117
+ ### Retrieve an intent
118
+
119
+ ```js
120
+ const intent = await client.tokenPurchaseIntents.retrieve('tpi_3LMnop');
121
+ ```
122
+
123
+ You can poll this to check status, or rely on webhooks — both work. Webhooks are preferred in production to avoid polling overhead.
124
+
125
+ ### List intents
126
+
127
+ ```js
128
+ const page = await client.tokenPurchaseIntents.list({
129
+ customer: 'cus_1JX9k2', // optional
130
+ status: 'succeeded', // optional: pending | succeeded | failed | canceled
131
+ limit: 25, // optional, 1-100, default 10
132
+ starting_after: 'tpi_3LMnop', // optional cursor — pass the last id from the previous page
133
+ });
134
+
135
+ // => { object: 'list', data: [...], has_more: false }
136
+ ```
137
+
138
+ ### Token amounts are 18-decimal integers — use BigInt
139
+
140
+ `token_amount_estimate` and `token_amount` are ERC-20 amounts in 18-decimal wei units and routinely exceed `Number.MAX_SAFE_INTEGER`. The API returns them as JSON numbers for readability, but you should re-parse them as `BigInt` before doing any arithmetic:
141
+
142
+ ```js
143
+ const wei = BigInt(intent.token_amount_estimate);
144
+ ```
145
+
146
+ `amount` and `price_snapshot`, by contrast, are ordinary human-readable decimals (`5.00` = $5.00) — don't multiply these by `10^6` or `10^18`.
147
+
148
+ ## Account
149
+
150
+ Read-only reference info for the account tied to your API key: current token price, contract addresses, accepted payment currencies, and active network.
151
+
152
+ ```js
153
+ const account = await client.account.retrieve();
154
+ // => {
155
+ // object: 'account',
156
+ // network: 'base-mainnet',
157
+ // token_contract: '0x...',
158
+ // swap_contract: '0x...',
159
+ // company_wallet: '0x...',
160
+ // price_per_token: 1.0,
161
+ // currency_contracts: { USDC: '0x...', USDT: '0x...' },
162
+ // livemode: true,
163
+ // }
164
+ ```
165
+
166
+ Call this on startup to confirm you're using the right key type and to discover which `payment_currency` values are currently valid — the keys of `currency_contracts`.
167
+
168
+ ## Webhooks
169
+
170
+ The create-intent response only ever means "accepted." Final settlement is asynchronous and delivered to your endpoint as one of four events:
171
+
172
+ | Event | Fired when |
173
+ |---|---|
174
+ | `token_purchase_intent.created` | A new intent is created. |
175
+ | `token_purchase_intent.succeeded` | The on-chain `Purchase` event is confirmed. Credit `data.object.token_amount` to the customer. |
176
+ | `token_purchase_intent.failed` | The on-chain transaction reverts or confirmation times out. `data.object.error` explains why. |
177
+ | `token_purchase_intent.canceled` | The intent expired unconfirmed after 30 minutes. |
178
+
179
+ Verify the signature and parse the payload with `client.webhooks.constructEvent` (also usable standalone via the named export, without constructing a client):
180
+
181
+ ```js
182
+ const { PlatformClient } = require('@shake-defi/node');
183
+ const client = new PlatformClient(process.env.PLATFORM_SECRET_KEY);
184
+
185
+ app.post('/webhooks/shake', express.raw({ type: 'application/json' }), (req, res) => {
186
+ let event;
187
+ try {
188
+ event = client.webhooks.constructEvent(
189
+ req.body, // raw request body (Buffer or string — NOT parsed JSON)
190
+ req.headers['platform-signature'],
191
+ process.env.PLATFORM_WEBHOOK_SECRET,
192
+ );
193
+ } catch (err) {
194
+ console.error('Webhook signature verification failed:', err.message);
195
+ return res.status(400).send('Invalid signature');
196
+ }
197
+
198
+ switch (event.type) {
199
+ case 'token_purchase_intent.succeeded': {
200
+ const intent = event.data.object;
201
+ // creditLedger(intent.customer, BigInt(intent.token_amount));
202
+ break;
203
+ }
204
+ case 'token_purchase_intent.failed':
205
+ case 'token_purchase_intent.canceled':
206
+ // notify the customer, optionally prompt a retry
207
+ break;
208
+ }
209
+
210
+ res.status(200).end(); // any 2xx acknowledges receipt
211
+ });
212
+ ```
213
+
214
+ Signatures are verified with HMAC-SHA256 over `{timestamp}.{raw_body}` and compared using a timing-safe check. Requests older than 5 minutes are rejected as possible replays. Return any non-2xx and the platform retries with exponential back-off (`5s → 30s → 5m → 30m → 2h`).
215
+
216
+ **Important:** the raw, unparsed request body is required for signature verification — make sure whatever body parser you use (e.g. `express.raw()`) doesn't JSON-parse this route before `constructEvent` sees it.
217
+
218
+ ## Error handling
219
+
220
+ Failed requests reject with a `PlatformApiError`:
221
+
222
+ ```js
223
+ const { PlatformApiError } = require('@shake-defi/node');
224
+
225
+ try {
226
+ await client.tokenPurchaseIntents.create({
227
+ customer: 'cus_1JX9k2',
228
+ amount: -5,
229
+ payment_currency: 'USDC',
230
+ });
231
+ } catch (err) {
232
+ if (err instanceof PlatformApiError) {
233
+ console.error(err.status); // 400
234
+ console.error(err.type); // 'invalid_request_error' | 'chain_error' | 'api_error'
235
+ console.error(err.code); // e.g. 'missing_required_param', 'insufficient_allowance'
236
+ console.error(err.param); // e.g. 'amount'
237
+ console.error(err.message); // human-readable explanation
238
+ }
239
+ }
240
+ ```
241
+
242
+ `chain_error` responses (surfaced when a widget confirmation ends in `failed`, and reflected in `intent.error`) use `code` to tell you what went wrong on-chain:
243
+
244
+ | Code | Meaning |
245
+ |---|---|
246
+ | `insufficient_allowance` | Customer hasn't approved enough of the payment currency. |
247
+ | `transaction_reverted` | The on-chain `buy` call reverted. |
248
+ | `price_changed` | Live price moved beyond tolerance between intent creation and confirmation. |
249
+ | `chain_congested` | RPC or mempool issues prevented submission. |
250
+ | `confirmation_timeout` | Required confirmations weren't reached within the expiry window. |
251
+
252
+ Webhook signature failures raise a separate `SignatureError`:
253
+
254
+ ```js
255
+ const { SignatureError } = require('@shake-defi/node');
256
+ ```
257
+
258
+ ## Idempotency
259
+
260
+ Pass an `Idempotency-Key` on `create` calls to make retries safe:
261
+
262
+ ```js
263
+ await client.tokenPurchaseIntents.create(params, { idempotencyKey: 'order_8841_attempt_1' });
264
+ ```
265
+
266
+ If the same key is received again before the original response expires, the original response is returned rather than creating a duplicate resource. Recommended format: `{order_id}_attempt_{n}`.
267
+
268
+ ## TypeScript
269
+
270
+ Type definitions are published alongside the package (`index.d.ts`) and resolve automatically — no `@types` package needed.
271
+
272
+ ## API reference
273
+
274
+ This SDK covers the full [Token Purchase Platform API](https://api.shake-defi.com):
275
+
276
+ | Method | HTTP |
277
+ |---|---|
278
+ | `client.customers.create(params, opts?)` | `POST /v1/customers` |
279
+ | `client.customers.retrieve(id)` | `GET /v1/customers/:id` |
280
+ | `client.tokenPurchaseIntents.create(params, opts?)` | `POST /v1/token_purchase_intents` |
281
+ | `client.tokenPurchaseIntents.retrieve(id)` | `GET /v1/token_purchase_intents/:id` |
282
+ | `client.tokenPurchaseIntents.list(params?)` | `GET /v1/token_purchase_intents` |
283
+ | `client.account.retrieve()` | `GET /v1/account` |
284
+ | `client.webhooks.constructEvent(rawBody, header, secret)` | verifies + parses inbound webhooks |
285
+
286
+ For full request/response schemas, field-level documentation, and the widget's public `GET /widget/params` endpoint (called by the checkout widget itself — you generally won't call it directly), see the OpenAPI spec.
287
+
288
+ ## License
289
+
290
+ Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shake-defi/node",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Node.js SDK for the Token Purchase Platform",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",