@buclepay/node 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +50 -2
- package/index.d.ts +108 -0
- package/index.js +105 -0
- package/package.json +46 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 buclePay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @buclepay/node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official Node.js SDK for [buclePay](https://buclepay.com): recurring payments in stablecoins (USDT, USDC) without custody. No dependencies, Node 18+.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i @buclepay/node
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Your API key (`bp_…`, Dashboard › Developers › API) lives **only on your server**, never in an app or a web page.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { BuclePay } from '@buclepay/node';
|
|
13
|
+
const bp = new BuclePay(process.env.BUCLEPAY_API_KEY);
|
|
14
|
+
|
|
15
|
+
// 1. Create a payment link with your own id, open link.url in your app or site
|
|
16
|
+
const { data: link } = await bp.paymentLinks.create({
|
|
17
|
+
amount_usd: 49.99, interval: 'month', description: 'Pro plan',
|
|
18
|
+
client_reference_id: 'user_123', metadata: { plan: 'pro' },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 2. Receive the signed webhook and activate the service
|
|
22
|
+
app.post('/webhooks/buclepay', express.raw({ type: 'application/json' }), (req, res) => {
|
|
23
|
+
const event = bp.webhooks.constructEvent(req.body.toString('utf8'), req.get('buclepay-signature'), process.env.BUCLEPAY_WHSEC);
|
|
24
|
+
if (event.type === 'payment.succeeded') activate(event.data.object.subscription.client_reference_id);
|
|
25
|
+
res.sendStatus(200);
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Method | What it does |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `paymentLinks.create / retrieve / list` | Payment links (one-time, plan or installments) |
|
|
32
|
+
| `subscriptions.retrieve / list` | Filter by `client_reference_id`, `email`, `status` |
|
|
33
|
+
| `plans.list`, `customers.list`, `payments.list` | Your plans, customers and payments |
|
|
34
|
+
| `events.list` | Same events as the webhooks |
|
|
35
|
+
| `perks.list / create`, `customers.perks(id)` | bucleScore rewards (below) |
|
|
36
|
+
| `webhooks.constructEvent / verifySignature` | Verify the `buclepay-signature` header |
|
|
37
|
+
|
|
38
|
+
## bucleScore: reward your best customers
|
|
39
|
+
|
|
40
|
+
Each payer has a bucleScore built from all their payments on buclePay. **You never see the score or the history.** You see the customer's **level (1–4)** only if they chose to show it to you, and you use it to give rewards: discounts (1–50%), free charges (1–3), gifts, priority or early access.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
await bp.perks.create({ min_level: 3, kind: 'discount_pct', value: 10, title: '10% off your renewal' });
|
|
44
|
+
const { data } = await bp.customers.perks('cus_…'); // { level_shared, level, perks }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The level is for rewards only. Never use it to deny service, ask for deposits, offer credit or installments, or charge differently.
|
|
48
|
+
|
|
49
|
+
Errors are thrown as `BuclePayError` with `status`, `code` and `param`.
|
|
50
|
+
|
|
51
|
+
License: MIT.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export class BuclePayError extends Error { status: number; code: string; param?: string }
|
|
2
|
+
|
|
3
|
+
export type Metadata = Record<string, string | number | boolean>;
|
|
4
|
+
|
|
5
|
+
export interface PaymentLinkParams {
|
|
6
|
+
/** Amount in USD (minimum 1.99, cents allowed). Not sent with `plan_id`. */
|
|
7
|
+
amount_usd?: number;
|
|
8
|
+
/** With `interval` the link is a subscription; without it, a one-time payment. */
|
|
9
|
+
interval?: 'day' | 'week' | 'month' | 'year';
|
|
10
|
+
/** day: 1–365 · week: 1 or 2 · month: 1 or 3 · year: 1 */
|
|
11
|
+
interval_count?: number;
|
|
12
|
+
/** Installments: charging stops by itself after the last one is paid (2–120). */
|
|
13
|
+
max_charges?: number;
|
|
14
|
+
trial_days?: number;
|
|
15
|
+
/** Subscribe to a plan created in the dashboard. */
|
|
16
|
+
plan_id?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
/** Up to 20 pairs; keys of 1–40 characters (letters, digits, _ . -); values up to 500. No personal data. */
|
|
19
|
+
metadata?: Metadata;
|
|
20
|
+
/** Your own id (user, order…, up to 200 characters). Comes back in the API and in every webhook for that subscription. */
|
|
21
|
+
client_reference_id?: string;
|
|
22
|
+
/** 1–90; otherwise your Settings › Charges default (3 days). */
|
|
23
|
+
expires_in_days?: number;
|
|
24
|
+
/** Where the customer returns when done: a public https:// URL or your app scheme (myapp://pay). bp_link, bp_status and bp_subscription are appended. */
|
|
25
|
+
success_url?: string;
|
|
26
|
+
/** Same, for "Cancel and go back" or when the payment fails (bp_status=canceled|failed). */
|
|
27
|
+
cancel_url?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PaymentLink {
|
|
31
|
+
id: string; object: 'payment_link'; url: string; type: 'once' | 'plan' | 'invoice' | 'open'; status: 'active' | 'expired' | 'disabled';
|
|
32
|
+
amount_usd: number; description: string | null; invoice_number: string | null; plan_id: string;
|
|
33
|
+
interval: string | null; interval_count: number | null; max_charges: number | null; trial_days: number;
|
|
34
|
+
expires_at: string | null; created_at: string; client_reference_id: string | null; metadata: Record<string, string>;
|
|
35
|
+
/** true once the customer has signed; `subscription_id` then points to their subscription. */
|
|
36
|
+
completed: boolean; subscription_id: string | null;
|
|
37
|
+
success_url: string | null; cancel_url: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Subscription {
|
|
41
|
+
id: string; object: 'subscription'; status: 'trialing' | 'active' | 'grace' | 'suspended' | 'canceled' | 'ended' | 'paused';
|
|
42
|
+
/** true = provide the service (paid up, in trial or in grace period). */
|
|
43
|
+
active: boolean;
|
|
44
|
+
plan: string; plan_id: string; amount_usd: number; interval: string; interval_count: number; max_charges: number | null; charges_made: number; token: 'USDT' | 'USDC' | null;
|
|
45
|
+
next_charge_at: string | null; trial_end: string | null; cancel_at_period_end: boolean; canceled_at: string | null; ended_at: string | null; created_at: string;
|
|
46
|
+
customer: { id: string; email: string | null }; label: string | null;
|
|
47
|
+
client_reference_id: string | null; metadata: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Invoice {
|
|
51
|
+
id: string; object: 'invoice'; number: string; status: string; subscription_id: string;
|
|
52
|
+
amount_usd: number; subtotal_usd: number; discount_usd: number; fee_usd: number; net_usd: number; fee_bps: number;
|
|
53
|
+
period_start: string; period_end: string; due_at: string; paid_at: string | null;
|
|
54
|
+
payment: { token: string | null; chain_id: number | null; chain: string | null; tx_hash: string; explorer_url: string | null } | null;
|
|
55
|
+
subscription: Subscription | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Plan {
|
|
59
|
+
id: string; object: 'plan'; name: string; amount_usd: number; interval: string; interval_count: number; trial_days: number;
|
|
60
|
+
max_charges: number | null; tokens: string[]; created_at: string; active_subscriptions: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type EventType =
|
|
64
|
+
| 'payment.succeeded' | 'payment.failed' | 'payment.retry_scheduled' | 'invoice.paid' | 'invoice.notice_undeliverable'
|
|
65
|
+
| 'subscription.created' | 'subscription.past_due' | 'subscription.suspended' | 'subscription.reactivated' | 'subscription.canceled' | 'subscription.ended'
|
|
66
|
+
| 'mandate.registered' | 'mandate.revoked' | 'merchant.restricted' | (string & {});
|
|
67
|
+
|
|
68
|
+
export interface BuclePayEvent {
|
|
69
|
+
id: string; type: EventType; created: number; api_version: string;
|
|
70
|
+
/** Event details (ids, reason…) plus `object`: the invoice (payment.*, invoice.*) or the subscription (subscription.*, mandate.*). */
|
|
71
|
+
data: Record<string, unknown> & { object?: Invoice | Subscription };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type PerkKind = 'discount_pct' | 'free_period' | 'gift' | 'priority' | 'early_access' | 'custom';
|
|
75
|
+
export interface Perk { id: string; object: 'perk'; min_level: 1 | 2 | 3 | 4; kind: PerkKind; value: number | null; title: string }
|
|
76
|
+
export interface PerkParams { min_level: 1 | 2 | 3 | 4; kind: PerkKind; /** discount_pct 1–50 · free_period 1–3 · the rest take no value */ value?: number; title: string }
|
|
77
|
+
export interface CustomerPerks { object: 'customer_perks'; customer: string; /** the customer agreed to show you their level */ level_shared: boolean; level: 1 | 2 | 3 | 4 | null; perks: Perk[] }
|
|
78
|
+
|
|
79
|
+
export class BuclePay {
|
|
80
|
+
constructor(apiKey: string, opts?: { baseUrl?: string; timeoutMs?: number; fetch?: typeof fetch });
|
|
81
|
+
paymentLinks: {
|
|
82
|
+
create(params: PaymentLinkParams): Promise<{ data: PaymentLink }>;
|
|
83
|
+
retrieve(id: string): Promise<{ data: PaymentLink }>;
|
|
84
|
+
list(params?: { client_reference_id?: string; limit?: number }): Promise<{ data: PaymentLink[] }>;
|
|
85
|
+
};
|
|
86
|
+
subscriptions: {
|
|
87
|
+
retrieve(id: string): Promise<{ data: Subscription }>;
|
|
88
|
+
list(params?: { client_reference_id?: string; email?: string; status?: string }): Promise<{ data: Subscription[] }>;
|
|
89
|
+
};
|
|
90
|
+
plans: { list(): Promise<{ data: Plan[] }> };
|
|
91
|
+
customers: {
|
|
92
|
+
list(): Promise<{ data: unknown[] }>;
|
|
93
|
+
/** Level (only if the customer agreed to show it to you) and the rewards it unlocks. Never the score or the history. */
|
|
94
|
+
perks(id: string): Promise<{ data: CustomerPerks }>;
|
|
95
|
+
};
|
|
96
|
+
/** Rewards by bucleScore level. Rewards only: never deposits, installments or credit terms. */
|
|
97
|
+
perks: { list(): Promise<{ data: Perk[] }>; create(params: PerkParams): Promise<{ data: Perk }> };
|
|
98
|
+
payments: { list(): Promise<{ data: unknown[] }> };
|
|
99
|
+
events: { list(params?: { type?: string; starting_after?: string; limit?: number }): Promise<{ data: BuclePayEvent[]; has_more: boolean }> };
|
|
100
|
+
webhooks: {
|
|
101
|
+
constructEvent(body: string, signatureHeader: string, secret: string, opts?: { toleranceSeconds?: number; now?: number }): BuclePayEvent;
|
|
102
|
+
verifySignature(body: string, signatureHeader: string, secret: string, opts?: { toleranceSeconds?: number; now?: number }): boolean;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function signPayload(secret: string, body: string, timestamp: number): string;
|
|
107
|
+
export function verifySignature(body: string, signatureHeader: string, secret: string, opts?: { toleranceSeconds?: number; now?: number }): boolean;
|
|
108
|
+
export function constructEvent(body: string, signatureHeader: string, secret: string, opts?: { toleranceSeconds?: number; now?: number }): BuclePayEvent;
|
package/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @buclepay/node — buclePay API v1 client and webhook verification. No dependencies (Node ≥ 18 fetch and crypto).
|
|
2
|
+
// Same style as Stripe: bp.paymentLinks.create(...), bp.subscriptions.retrieve(...), bp.webhooks.constructEvent(...).
|
|
3
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
export class BuclePayError extends Error {
|
|
6
|
+
constructor(message, { status = 0, code = 'error', param = undefined } = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'BuclePayError';
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.param = param;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const qs = (o = {}) => {
|
|
16
|
+
const p = new URLSearchParams();
|
|
17
|
+
for (const [k, v] of Object.entries(o)) if (v !== undefined && v !== null && v !== '') p.set(k, String(v));
|
|
18
|
+
const s = p.toString();
|
|
19
|
+
return s ? `?${s}` : '';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class BuclePay {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} apiKey `bp_…` key (Dashboard › Developers › API). Server only: never in an app or a web page.
|
|
25
|
+
* @param {{ baseUrl?: string, timeoutMs?: number, fetch?: typeof fetch }} [opts]
|
|
26
|
+
*/
|
|
27
|
+
constructor(apiKey, opts = {}) {
|
|
28
|
+
if (typeof apiKey !== 'string' || !/^bp_[A-Za-z0-9_-]{20,80}$/.test(apiKey)) throw new BuclePayError('Missing or malformed API key (bp_…)', { code: 'invalid_api_key' });
|
|
29
|
+
this.apiKey = apiKey;
|
|
30
|
+
this.baseUrl = (opts.baseUrl ?? 'https://app.buclepay.com').replace(/\/$/, '');
|
|
31
|
+
this.timeoutMs = opts.timeoutMs ?? 15_000;
|
|
32
|
+
this._fetch = opts.fetch ?? globalThis.fetch;
|
|
33
|
+
|
|
34
|
+
this.paymentLinks = {
|
|
35
|
+
/** A one-time payment (`amount_usd`), a subscription with its own price (`amount_usd` + `interval`) or one of your plans (`plan_id`). */
|
|
36
|
+
create: (params) => this._request('POST', '/payment_links', params),
|
|
37
|
+
retrieve: (id) => this._request('GET', `/payment_links/${encodeURIComponent(id)}`),
|
|
38
|
+
list: (params) => this._request('GET', `/payment_links${qs(params)}`),
|
|
39
|
+
};
|
|
40
|
+
this.subscriptions = {
|
|
41
|
+
retrieve: (id) => this._request('GET', `/subscriptions/${encodeURIComponent(id)}`),
|
|
42
|
+
/** Filters: `client_reference_id`, `email`, `status`. */
|
|
43
|
+
list: (params) => this._request('GET', `/subscriptions${qs(params)}`),
|
|
44
|
+
};
|
|
45
|
+
this.plans = { list: () => this._request('GET', '/plans') };
|
|
46
|
+
this.customers = {
|
|
47
|
+
list: () => this._request('GET', '/customers'),
|
|
48
|
+
/** Level (only if the customer agreed to show it to you) and the rewards it unlocks. Never the score or the history. */
|
|
49
|
+
perks: (id) => this._request('GET', `/customers/${encodeURIComponent(id)}/perks`),
|
|
50
|
+
};
|
|
51
|
+
this.perks = {
|
|
52
|
+
/** Rewards by bucleScore level that you define. Rewards only: never deposits, installments or credit terms. */
|
|
53
|
+
list: () => this._request('GET', '/perks'),
|
|
54
|
+
create: (params) => this._request('POST', '/perks', params),
|
|
55
|
+
};
|
|
56
|
+
this.payments = { list: () => this._request('GET', '/payments') };
|
|
57
|
+
this.events = {
|
|
58
|
+
/** The same events sent by webhook; use it to catch up after downtime. Cursor: `starting_after`. */
|
|
59
|
+
list: (params) => this._request('GET', `/events${qs(params)}`),
|
|
60
|
+
};
|
|
61
|
+
this.webhooks = { constructEvent: constructEvent, verifySignature: verifySignature };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async _request(method, path, body) {
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
res = await this._fetch(`${this.baseUrl}/api/v1${path}`, {
|
|
68
|
+
method,
|
|
69
|
+
headers: { authorization: `Bearer ${this.apiKey}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }), 'user-agent': '@buclepay/node/0.1' },
|
|
70
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
71
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
throw new BuclePayError(`Could not reach buclePay: ${e?.message ?? e}`, { code: 'connection_error' });
|
|
75
|
+
}
|
|
76
|
+
let json = null;
|
|
77
|
+
try { json = await res.json(); } catch { /* empty or non-JSON body */ }
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
const err = json?.error ?? {};
|
|
80
|
+
throw new BuclePayError(err.message ?? `HTTP ${res.status}`, { status: res.status, code: err.code ?? 'error', param: err.param });
|
|
81
|
+
}
|
|
82
|
+
return json;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Webhook signature: header `buclepay-signature: t=<unix>,v1=<hmac_sha256(secret, "<t>.<body>")>`. */
|
|
87
|
+
export function signPayload(secret, body, timestamp) {
|
|
88
|
+
return `t=${timestamp},v1=${createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** true if the signature is valid and recent (5 min by default). `body` must be the EXACT raw text received; never re-serialize the JSON. */
|
|
92
|
+
export function verifySignature(body, header, secret, { toleranceSeconds = 300, now = Date.now() } = {}) {
|
|
93
|
+
const m = /t=(\d+),v1=([0-9a-f]{64})/.exec(String(header ?? ''));
|
|
94
|
+
if (!m || typeof body !== 'string' || !secret) return false;
|
|
95
|
+
if (Math.abs(now / 1000 - Number(m[1])) > toleranceSeconds) return false;
|
|
96
|
+
const want = Buffer.from(signPayload(secret, body, Number(m[1])).split('v1=')[1]);
|
|
97
|
+
const got = Buffer.from(m[2]);
|
|
98
|
+
return want.length === got.length && timingSafeEqual(want, got);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Verifies the signature and returns the parsed event; throws BuclePayError if anything is off (reply 400 and process nothing). */
|
|
102
|
+
export function constructEvent(body, header, secret, opts) {
|
|
103
|
+
if (!verifySignature(body, header, secret, opts)) throw new BuclePayError('Invalid or expired webhook signature', { code: 'invalid_signature', status: 400 });
|
|
104
|
+
try { return JSON.parse(body); } catch { throw new BuclePayError('Webhook body is not JSON', { code: 'invalid_payload', status: 400 }); }
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buclepay/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "buclePay for Node: create payment links, read subscriptions, verify webhooks and manage bucleScore rewards. No dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"index.d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://buclepay.com",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/buclepay/buclepay.git",
|
|
28
|
+
"directory": "sdk/node"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/buclepay/buclepay/issues"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"buclepay",
|
|
35
|
+
"payments",
|
|
36
|
+
"recurring",
|
|
37
|
+
"subscriptions",
|
|
38
|
+
"stablecoins",
|
|
39
|
+
"usdt",
|
|
40
|
+
"usdc",
|
|
41
|
+
"crypto",
|
|
42
|
+
"webhooks",
|
|
43
|
+
"non-custodial"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "node --test"
|
|
47
|
+
}
|
|
48
|
+
}
|