@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 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) {