@relai-fi/x402 0.5.31 → 0.5.32
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 +102 -0
- package/dist/index.cjs +64 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins.cjs +150 -0
- package/dist/plugins.cjs.map +1 -0
- package/dist/plugins.d.cts +2 -0
- package/dist/plugins.d.ts +2 -0
- package/dist/plugins.js +125 -0
- package/dist/plugins.js.map +1 -0
- package/dist/server-B89hUwBK.d.ts +229 -0
- package/dist/server-DqvJNI2_.d.cts +229 -0
- package/dist/server.cjs +64 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -138
- package/dist/server.d.ts +2 -138
- package/dist/server.js +64 -1
- package/dist/server.js.map +1 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -340,6 +340,9 @@ import {
|
|
|
340
340
|
fromAtomicUnits,
|
|
341
341
|
} from '@relai-fi/x402/utils';
|
|
342
342
|
|
|
343
|
+
// Plugins — extend protect() with free tier, custom logic
|
|
344
|
+
import { freeTier } from '@relai-fi/x402/plugins';
|
|
345
|
+
|
|
343
346
|
// Management API — create/manage APIs, pricing, analytics, agent bootstrap
|
|
344
347
|
import {
|
|
345
348
|
createManagementClient,
|
|
@@ -508,6 +511,105 @@ app.get('/api/solana-data', relai.protect({
|
|
|
508
511
|
|
|
509
512
|
---
|
|
510
513
|
|
|
514
|
+
## Plugins
|
|
515
|
+
|
|
516
|
+
Extend `Relai.protect()` with plugins that run before payment checks. Plugins can skip payment (e.g. free tier), add headers, or attach metadata to requests.
|
|
517
|
+
|
|
518
|
+
### Free Tier Plugin
|
|
519
|
+
|
|
520
|
+
Allow buyers to make free API calls before requiring x402 payment. Usage is tracked per buyer (by JWT `sub`, wallet address, or IP) with optional global caps and periodic resets.
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import Relai from '@relai-fi/x402/server';
|
|
524
|
+
import { freeTier } from '@relai-fi/x402/plugins';
|
|
525
|
+
|
|
526
|
+
const relai = new Relai({
|
|
527
|
+
network: 'base',
|
|
528
|
+
plugins: [
|
|
529
|
+
freeTier({
|
|
530
|
+
serviceKey: process.env.RELAI_SERVICE_KEY!,
|
|
531
|
+
perBuyerLimit: 10, // 10 free calls per buyer
|
|
532
|
+
resetPeriod: 'daily', // reset daily (or 'monthly', 'never')
|
|
533
|
+
globalCap: 1000, // optional: max 1000 free calls total
|
|
534
|
+
paths: ['*'], // optional: apply to all endpoints (default)
|
|
535
|
+
}),
|
|
536
|
+
],
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
app.get('/api/data', relai.protect({
|
|
540
|
+
payTo: '0xYourWallet',
|
|
541
|
+
price: 0.01,
|
|
542
|
+
}), (req, res) => {
|
|
543
|
+
if (req.x402Free) {
|
|
544
|
+
// Free tier call — no payment was made
|
|
545
|
+
console.log('Free call from:', req.x402Plugin);
|
|
546
|
+
}
|
|
547
|
+
res.json({ data: 'content' });
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**How it works:**
|
|
552
|
+
1. On server start, the plugin syncs its config to the RelAI backend via your service key.
|
|
553
|
+
2. On each request, `beforePaymentCheck` asks the RelAI API if the buyer has free calls remaining.
|
|
554
|
+
3. If free → `next()` is called without payment, `req.x402Free = true`, and usage is recorded.
|
|
555
|
+
4. If exhausted → normal x402 payment flow continues.
|
|
556
|
+
|
|
557
|
+
**Free Tier config:**
|
|
558
|
+
|
|
559
|
+
| Option | Type | Default | Description |
|
|
560
|
+
|--------|------|---------|-------------|
|
|
561
|
+
| `serviceKey` | `string` | **required** | Your RelAI service key (`sk_live_...`) |
|
|
562
|
+
| `perBuyerLimit` | `number` | **required** | Free calls each buyer gets per period |
|
|
563
|
+
| `resetPeriod` | `'never' \| 'daily' \| 'monthly'` | `'never'` | When counters reset |
|
|
564
|
+
| `globalCap` | `number` | — | Max total free calls across all buyers |
|
|
565
|
+
| `paths` | `string[]` | `['*']` | Which endpoints the free tier applies to |
|
|
566
|
+
| `baseUrl` | `string` | `https://api.relai.fi` | RelAI API URL (override for testing) |
|
|
567
|
+
|
|
568
|
+
**Request properties set on free-tier bypass:**
|
|
569
|
+
|
|
570
|
+
| Property | Type | Description |
|
|
571
|
+
|----------|------|-------------|
|
|
572
|
+
| `req.x402Free` | `boolean` | `true` when request was served for free |
|
|
573
|
+
| `req.x402Paid` | `boolean` | `false` on free tier, `true` on paid |
|
|
574
|
+
| `req.x402Plugin` | `string` | Plugin name that granted the bypass (`'freeTier'`) |
|
|
575
|
+
| `req.pluginMeta` | `object` | `{ freeTier: true, remaining: number }` |
|
|
576
|
+
|
|
577
|
+
**Dashboard:** Manage plugin config and view usage in the RelAI dashboard under **SDK Plugins**.
|
|
578
|
+
|
|
579
|
+
### Custom Plugins
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
import type { RelaiPlugin } from '@relai-fi/x402';
|
|
583
|
+
|
|
584
|
+
const myPlugin: RelaiPlugin = {
|
|
585
|
+
name: 'myPlugin',
|
|
586
|
+
beforePaymentCheck: async (ctx) => {
|
|
587
|
+
if (ctx.req.headers['x-vip'] === 'true') {
|
|
588
|
+
return { skip: true, reason: 'VIP bypass' };
|
|
589
|
+
}
|
|
590
|
+
return {};
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const relai = new Relai({
|
|
595
|
+
network: 'base',
|
|
596
|
+
plugins: [myPlugin],
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
**Plugin interface:**
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
interface RelaiPlugin {
|
|
604
|
+
name: string;
|
|
605
|
+
beforePaymentCheck?: (ctx: PluginContext) => Promise<PluginResult>;
|
|
606
|
+
afterSettled?: (ctx: PluginContext) => Promise<void>;
|
|
607
|
+
onInit?: (config: RelaiServerConfig) => Promise<void>;
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
511
613
|
## Management API
|
|
512
614
|
|
|
513
615
|
Programmatically create and manage monetised APIs, update pricing, and read analytics. Designed for agents and CI/CD — no browser needed.
|
package/dist/index.cjs
CHANGED
|
@@ -508,11 +508,30 @@ async function createStripeDepositAddress(secretKey, amountUsdCents, network = "
|
|
|
508
508
|
return address;
|
|
509
509
|
}
|
|
510
510
|
var Relai = class {
|
|
511
|
-
// Cache feePayer per network
|
|
512
511
|
constructor(config) {
|
|
513
512
|
this.feePayerCache = /* @__PURE__ */ new Map();
|
|
513
|
+
this.pluginsInitialized = false;
|
|
514
514
|
this.network = config.network;
|
|
515
515
|
this.facilitatorUrl = config.facilitatorUrl || RELAI_FACILITATOR_URL;
|
|
516
|
+
this.plugins = config.plugins ?? [];
|
|
517
|
+
if (this.plugins.length > 0) {
|
|
518
|
+
this.initPlugins().catch((err) => {
|
|
519
|
+
console.warn("[Relai] Plugin initialization error (non-blocking):", err);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async initPlugins() {
|
|
524
|
+
if (this.pluginsInitialized) return;
|
|
525
|
+
for (const plugin of this.plugins) {
|
|
526
|
+
if (plugin.onInit) {
|
|
527
|
+
try {
|
|
528
|
+
await plugin.onInit();
|
|
529
|
+
} catch (err) {
|
|
530
|
+
console.warn(`[Relai] Plugin '${plugin.name}' init failed:`, err);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
this.pluginsInitialized = true;
|
|
516
535
|
}
|
|
517
536
|
/**
|
|
518
537
|
* Get feePayer address for a network (cached)
|
|
@@ -603,6 +622,34 @@ var Relai = class {
|
|
|
603
622
|
const integritasFlow = headerIntegritasFlow || configuredIntegritas.flow;
|
|
604
623
|
const integritasMode = integritasFlow === "single" ? "single_signature_fee_included" : integritasFlow === "dual" ? "dual_signature_split" : void 0;
|
|
605
624
|
const paymentHeader = req.headers["x-payment"] || req.headers["payment-signature"] || req.headers["x-payment-signature"];
|
|
625
|
+
if (!paymentHeader && self.plugins.length > 0) {
|
|
626
|
+
const pluginCtx = {
|
|
627
|
+
network,
|
|
628
|
+
price: resolvedPrice,
|
|
629
|
+
path: req.path || req.originalUrl || "/",
|
|
630
|
+
method: (req.method || "GET").toUpperCase()
|
|
631
|
+
};
|
|
632
|
+
for (const plugin of self.plugins) {
|
|
633
|
+
if (!plugin.beforePaymentCheck) continue;
|
|
634
|
+
try {
|
|
635
|
+
const pluginResult = await plugin.beforePaymentCheck(req, pluginCtx);
|
|
636
|
+
if (pluginResult?.skip) {
|
|
637
|
+
if (pluginResult.headers) {
|
|
638
|
+
for (const [k, v] of Object.entries(pluginResult.headers)) {
|
|
639
|
+
res.setHeader(k, v);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
req.pluginMeta = { ...req.pluginMeta || {}, ...pluginResult.meta || {} };
|
|
643
|
+
req.x402Paid = false;
|
|
644
|
+
req.x402Free = true;
|
|
645
|
+
req.x402Plugin = plugin.name;
|
|
646
|
+
return next();
|
|
647
|
+
}
|
|
648
|
+
} catch (pluginErr) {
|
|
649
|
+
console.warn(`[Relai] Plugin '${plugin.name}' beforePaymentCheck error (non-blocking):`, pluginErr);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
606
653
|
if (!paymentHeader) {
|
|
607
654
|
options.onPaymentRequired?.(req, { price: resolvedPrice, network });
|
|
608
655
|
let resolvedPayTo;
|
|
@@ -741,6 +788,22 @@ var Relai = class {
|
|
|
741
788
|
Buffer.from(JSON.stringify(paymentResponse)).toString("base64")
|
|
742
789
|
);
|
|
743
790
|
options.onPaymentSettled?.(req, result);
|
|
791
|
+
if (self.plugins.length > 0) {
|
|
792
|
+
const settleCtx = {
|
|
793
|
+
network,
|
|
794
|
+
price: resolvedPrice,
|
|
795
|
+
path: req.path || req.originalUrl || "/",
|
|
796
|
+
method: (req.method || "GET").toUpperCase()
|
|
797
|
+
};
|
|
798
|
+
for (const plugin of self.plugins) {
|
|
799
|
+
if (!plugin.afterSettled) continue;
|
|
800
|
+
try {
|
|
801
|
+
await plugin.afterSettled(req, result, settleCtx);
|
|
802
|
+
} catch (pluginErr) {
|
|
803
|
+
console.warn(`[Relai] Plugin '${plugin.name}' afterSettled error (non-blocking):`, pluginErr);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
744
807
|
if (options.customRules) {
|
|
745
808
|
const valid = await options.customRules(req);
|
|
746
809
|
if (!valid) {
|