@relai-fi/x402 0.5.39 → 0.6.0-rc.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.
- package/README.md +380 -21
- package/dist/index.cjs +100 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +99 -6
- package/dist/index.js.map +1 -1
- package/dist/plugins.cjs +21467 -45
- package/dist/plugins.cjs.map +1 -1
- package/dist/plugins.d.cts +2 -1
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +21477 -33
- package/dist/plugins.js.map +1 -1
- package/dist/relay-feedback.cjs +86 -0
- package/dist/relay-feedback.cjs.map +1 -0
- package/dist/relay-feedback.d.cts +63 -0
- package/dist/relay-feedback.d.ts +63 -0
- package/dist/relay-feedback.js +61 -0
- package/dist/relay-feedback.js.map +1 -0
- package/dist/server-Dr3JOA0-.d.ts +713 -0
- package/dist/server-t9nKvoKl.d.cts +713 -0
- package/dist/server.cjs +40 -6
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +40 -6
- package/dist/server.js.map +1 -1
- package/package.json +10 -1
- package/dist/server-CaSmhDnd.d.ts +0 -312
- package/dist/server-CyfEHW9D.d.cts +0 -312
package/README.md
CHANGED
|
@@ -340,8 +340,8 @@ import {
|
|
|
340
340
|
fromAtomicUnits,
|
|
341
341
|
} from '@relai-fi/x402/utils';
|
|
342
342
|
|
|
343
|
-
// Plugins — extend protect() with
|
|
344
|
-
import { freeTier } from '@relai-fi/x402/plugins';
|
|
343
|
+
// Plugins — extend protect() with built-in & custom logic
|
|
344
|
+
import { freeTier, bridge, shield, preflight, circuitBreaker, refund } from '@relai-fi/x402/plugins';
|
|
345
345
|
|
|
346
346
|
// Management API — create/manage APIs, pricing, analytics, agent bootstrap
|
|
347
347
|
import {
|
|
@@ -513,7 +513,20 @@ app.get('/api/solana-data', relai.protect({
|
|
|
513
513
|
|
|
514
514
|
## Plugins
|
|
515
515
|
|
|
516
|
-
Extend `Relai.protect()` with plugins that
|
|
516
|
+
Extend `Relai.protect()` with plugins that hook into the payment lifecycle. Six built-in plugins ship with the SDK:
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { freeTier, bridge, shield, preflight, circuitBreaker, refund } from '@relai-fi/x402/plugins';
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
| Plugin | Purpose | Hook |
|
|
523
|
+
|--------|---------|------|
|
|
524
|
+
| **freeTier** | Free API calls before payment | `beforePaymentCheck` → `skip` |
|
|
525
|
+
| **bridge** | Cross-chain payments (Solana ↔ SKALE ↔ Base) | `enrich402Response` |
|
|
526
|
+
| **shield** | Global service health check before payment | `beforePaymentCheck` → `reject` |
|
|
527
|
+
| **preflight** | Per-endpoint liveness probe before payment | `beforePaymentCheck` → `reject` |
|
|
528
|
+
| **circuitBreaker** | Failure history tracking, auto-open circuit | `beforePaymentCheck` → `reject` + `afterSettled` |
|
|
529
|
+
| **refund** | Auto-credit buyers when paid requests fail | `beforePaymentCheck` → `skip` + `afterSettled` |
|
|
517
530
|
|
|
518
531
|
### Free Tier Plugin
|
|
519
532
|
|
|
@@ -554,16 +567,15 @@ app.get('/api/data', relai.protect({
|
|
|
554
567
|
3. If free → `next()` is called without payment, `req.x402Free = true`, and usage is recorded.
|
|
555
568
|
4. If exhausted → normal x402 payment flow continues.
|
|
556
569
|
|
|
557
|
-
**
|
|
570
|
+
**Config:**
|
|
558
571
|
|
|
559
572
|
| Option | Type | Default | Description |
|
|
560
573
|
|--------|------|---------|-------------|
|
|
561
|
-
| `serviceKey` | `string` |
|
|
574
|
+
| `serviceKey` | `string` | — | Your `sk_live_...` key. Omit for local in-memory mode. |
|
|
562
575
|
| `perBuyerLimit` | `number` | **required** | Free calls each buyer gets per period |
|
|
563
|
-
| `resetPeriod` | `'
|
|
576
|
+
| `resetPeriod` | `'none' \| 'daily' \| 'monthly'` | `'none'` | When counters reset |
|
|
564
577
|
| `globalCap` | `number` | — | Max total free calls across all buyers |
|
|
565
578
|
| `paths` | `string[]` | `['*']` | Which endpoints the free tier applies to |
|
|
566
|
-
| `baseUrl` | `string` | `https://api.relai.fi` | RelAI API URL (override for testing) |
|
|
567
579
|
|
|
568
580
|
**Request properties set on free-tier bypass:**
|
|
569
581
|
|
|
@@ -574,27 +586,357 @@ app.get('/api/data', relai.protect({
|
|
|
574
586
|
| `req.x402Plugin` | `string` | Plugin name that granted the bypass (`'freeTier'`) |
|
|
575
587
|
| `req.pluginMeta` | `object` | `{ freeTier: true, remaining: number }` |
|
|
576
588
|
|
|
577
|
-
|
|
589
|
+
### Bridge Plugin
|
|
590
|
+
|
|
591
|
+
Accept cross-chain payments. Buyers on Solana can pay your SKALE Base API — the SDK handles bridging automatically.
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import Relai from '@relai-fi/x402/server';
|
|
595
|
+
import { bridge } from '@relai-fi/x402/plugins';
|
|
596
|
+
|
|
597
|
+
const relai = new Relai({
|
|
598
|
+
network: 'skale-bite',
|
|
599
|
+
plugins: [
|
|
600
|
+
bridge({ serviceKey: process.env.RELAI_SERVICE_KEY }),
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
The plugin auto-discovers bridge capabilities from `/bridge/info`. No manual chain configuration needed.
|
|
606
|
+
|
|
607
|
+
| Option | Type | Default | Description |
|
|
608
|
+
|--------|------|---------|-------------|
|
|
609
|
+
| `serviceKey` | `string` | — | Recommended. Tracks bridge usage in dashboard. |
|
|
610
|
+
| `settleEndpoint` | `string` | `/bridge/settle` | Custom settle endpoint |
|
|
611
|
+
| `feeBps` | `number` | `100` | Bridge fee in basis points (100 = 1%) |
|
|
612
|
+
|
|
613
|
+
### Shield Plugin
|
|
614
|
+
|
|
615
|
+
Global service health check — protects buyers from paying for unhealthy endpoints. Before the server returns 402, Shield runs a health check. If unhealthy, returns 503 instead of asking for payment.
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
import Relai from '@relai-fi/x402/server';
|
|
619
|
+
import { shield } from '@relai-fi/x402/plugins';
|
|
620
|
+
|
|
621
|
+
const relai = new Relai({
|
|
622
|
+
network: 'base',
|
|
623
|
+
plugins: [
|
|
624
|
+
shield({
|
|
625
|
+
healthUrl: 'https://my-api.com/health',
|
|
626
|
+
timeoutMs: 3000,
|
|
627
|
+
}),
|
|
628
|
+
// Or use a custom function:
|
|
629
|
+
// shield({
|
|
630
|
+
// healthCheck: async () => {
|
|
631
|
+
// const dbOk = await checkDatabase();
|
|
632
|
+
// return dbOk;
|
|
633
|
+
// },
|
|
634
|
+
// }),
|
|
635
|
+
],
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
| Option | Type | Default | Description |
|
|
640
|
+
|--------|------|---------|-------------|
|
|
641
|
+
| `healthUrl` | `string` | — | URL to probe. 2xx = healthy. |
|
|
642
|
+
| `healthCheck` | `() => boolean \| Promise<boolean>` | — | Custom function. Takes priority over `healthUrl`. |
|
|
643
|
+
| `timeoutMs` | `number` | `5000` | Timeout for health probe (ms) |
|
|
644
|
+
| `cacheTtlMs` | `number` | `10000` | Cache health result (ms) |
|
|
645
|
+
| `unhealthyStatus` | `number` | `503` | HTTP status when unhealthy |
|
|
646
|
+
| `unhealthyMessage` | `string` | `Service temporarily unavailable...` | Error message |
|
|
647
|
+
|
|
648
|
+
**Response headers:** `X-Shield-Status: healthy|unhealthy`, `Retry-After` (when unhealthy).
|
|
649
|
+
|
|
650
|
+
### Preflight Plugin
|
|
651
|
+
|
|
652
|
+
Per-endpoint liveness probe — verifies the **specific endpoint** responds before payment. Sends `HEAD` with `X-Preflight: true` header; the middleware responds 200 instantly without triggering payment.
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
import Relai from '@relai-fi/x402/server';
|
|
656
|
+
import { preflight } from '@relai-fi/x402/plugins';
|
|
657
|
+
|
|
658
|
+
const relai = new Relai({
|
|
659
|
+
network: 'base',
|
|
660
|
+
plugins: [
|
|
661
|
+
preflight({ baseUrl: 'https://my-api.com' }),
|
|
662
|
+
],
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// If /api/data doesn't respond, buyers get 503 — never 402
|
|
666
|
+
app.get('/api/data', relai.protect({
|
|
667
|
+
payTo: '0xYourWallet',
|
|
668
|
+
price: 0.01,
|
|
669
|
+
}), handler);
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
| Option | Type | Default | Description |
|
|
673
|
+
|--------|------|---------|-------------|
|
|
674
|
+
| `baseUrl` | `string` | **required** | Base URL of the API. Request path is appended automatically. |
|
|
675
|
+
| `timeoutMs` | `number` | `3000` | Timeout for probe (ms) |
|
|
676
|
+
| `cacheTtlMs` | `number` | `5000` | Cache per-path result (ms) |
|
|
677
|
+
| `unhealthyStatus` | `number` | `503` | HTTP status when unreachable |
|
|
678
|
+
| `unhealthyMessage` | `string` | `Endpoint not responding...` | Error message |
|
|
679
|
+
|
|
680
|
+
**Response headers:** `X-Preflight-Status: ok|unreachable`, `Retry-After` (when unreachable).
|
|
681
|
+
|
|
682
|
+
**Shield vs Preflight:**
|
|
683
|
+
|
|
684
|
+
| | Shield | Preflight |
|
|
685
|
+
|---|--------|-----------|
|
|
686
|
+
| Scope | Global service health | Per-endpoint liveness |
|
|
687
|
+
| Probe target | Separate health URL / function | The actual protected endpoint |
|
|
688
|
+
| Cache | Single result (10s) | Per-path (5s) |
|
|
689
|
+
| Use case | DB/Redis/external API down | Specific endpoint not responding |
|
|
690
|
+
|
|
691
|
+
### Circuit Breaker Plugin
|
|
692
|
+
|
|
693
|
+
Tracks failure history and automatically "opens the circuit" after repeated failures — preventing buyers from paying for broken endpoints. **Zero-latency** — no extra HTTP requests.
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
import Relai from '@relai-fi/x402/server';
|
|
697
|
+
import { circuitBreaker } from '@relai-fi/x402/plugins';
|
|
698
|
+
|
|
699
|
+
const relai = new Relai({
|
|
700
|
+
network: 'base',
|
|
701
|
+
plugins: [
|
|
702
|
+
circuitBreaker({
|
|
703
|
+
failureThreshold: 5, // open after 5 failures
|
|
704
|
+
resetTimeMs: 30000, // try again after 30s
|
|
705
|
+
}),
|
|
706
|
+
],
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**States:** closed (normal) → open (all rejected 503) → half-open (test requests) → closed.
|
|
711
|
+
|
|
712
|
+
| Option | Type | Default | Description |
|
|
713
|
+
|--------|------|---------|-------------|
|
|
714
|
+
| `failureThreshold` | `number` | `5` | Consecutive failures before circuit opens |
|
|
715
|
+
| `resetTimeMs` | `number` | `30000` | Time (ms) circuit stays open before half-open |
|
|
716
|
+
| `halfOpenSuccesses` | `number` | `2` | Successes needed in half-open to close |
|
|
717
|
+
| `openStatus` | `number` | `503` | HTTP status when circuit is open |
|
|
718
|
+
| `openMessage` | `string` | `Service temporarily unavailable...` | Error message |
|
|
719
|
+
| `failureCodes` | `number[]` | `[500, 502, 503, 504]` | HTTP codes treated as failures |
|
|
720
|
+
| `scope` | `'global' \| 'per-path'` | `'per-path'` | Track globally or per endpoint |
|
|
721
|
+
|
|
722
|
+
**Response headers:** `X-Circuit-State: closed|open|half-open`, `Retry-After` (when open).
|
|
723
|
+
|
|
724
|
+
### Refund Plugin
|
|
725
|
+
|
|
726
|
+
Automatically compensates buyers when paid requests fail. Records an in-memory credit or calls your custom handler.
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
import Relai from '@relai-fi/x402/server';
|
|
730
|
+
import { refund } from '@relai-fi/x402/plugins';
|
|
731
|
+
|
|
732
|
+
const relai = new Relai({
|
|
733
|
+
network: 'base',
|
|
734
|
+
plugins: [
|
|
735
|
+
refund({
|
|
736
|
+
triggerCodes: [500, 502, 503],
|
|
737
|
+
mode: 'credit',
|
|
738
|
+
onRefund: (event) => {
|
|
739
|
+
console.log(`Refund: $${event.amount} to ${event.payer}`);
|
|
740
|
+
},
|
|
741
|
+
}),
|
|
742
|
+
],
|
|
743
|
+
});
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**Modes:**
|
|
747
|
+
- `credit` — records credit per buyer. Next request skips payment automatically. Header: `X-Refund-Credit: applied`.
|
|
748
|
+
- `log` — only calls `onRefund`. Handle refunds externally (DB, Stripe, etc.).
|
|
749
|
+
|
|
750
|
+
| Option | Type | Default | Description |
|
|
751
|
+
|--------|------|---------|-------------|
|
|
752
|
+
| `triggerCodes` | `number[]` | `[500, 502, 503, 504]` | HTTP codes that trigger a refund |
|
|
753
|
+
| `mode` | `'credit' \| 'log'` | `'credit'` | Auto-credit or callback-only |
|
|
754
|
+
| `onRefund` | `(event: RefundEvent) => void` | — | Callback on every refund event |
|
|
755
|
+
| `refundOnSettlementFailure` | `boolean` | `true` | Also refund when settlement itself fails |
|
|
756
|
+
|
|
757
|
+
### ERC-8004 Reputation Plugins
|
|
758
|
+
|
|
759
|
+
Build verifiable on-chain reputation for your API using the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) standard. Scores are stored on SKALE Base Sepolia (zero-cost transactions) and readable by any agent before payment.
|
|
760
|
+
|
|
761
|
+
#### `score()` — Inject reputation into the 402 response
|
|
762
|
+
|
|
763
|
+
Before an agent pays, it can see your API's live on-chain score in the `extensions.score` field of the 402 response. Fetches directly from the SKALE RPC node — no REST API.
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
import Relai from '@relai-fi/x402/server';
|
|
767
|
+
import { score } from '@relai-fi/x402/plugins';
|
|
768
|
+
|
|
769
|
+
const relai = new Relai({
|
|
770
|
+
network: 'base',
|
|
771
|
+
plugins: [
|
|
772
|
+
score({ agentId: process.env.ERC8004_AGENT_ID! }),
|
|
773
|
+
],
|
|
774
|
+
});
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
The 402 response will include:
|
|
778
|
+
```json
|
|
779
|
+
{
|
|
780
|
+
"extensions": {
|
|
781
|
+
"score": {
|
|
782
|
+
"agentId": "5",
|
|
783
|
+
"feedbackCount": 142,
|
|
784
|
+
"successRate": 98.6,
|
|
785
|
+
"avgResponseMs": 312
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
| Option | Type | Default | Description |
|
|
792
|
+
|--------|------|---------|-------------|
|
|
793
|
+
| `agentId` | `string \| number` | — | ERC-8004 NFT token ID from your dashboard |
|
|
794
|
+
| `rpcUrl` | `string` | `process.env.ERC8004_RPC_URL` | SKALE Base Sepolia RPC URL |
|
|
795
|
+
| `identityRegistryAddress` | `string` | `process.env.ERC8004_IDENTITY_REGISTRY` | IdentityRegistry contract address |
|
|
796
|
+
| `reputationRegistryAddress` | `string` | `process.env.ERC8004_REPUTATION_REGISTRY` | ReputationRegistry contract address |
|
|
797
|
+
| `cacheTtlMs` | `number` | `300000` | Score cache TTL (default: 5 min) |
|
|
798
|
+
|
|
799
|
+
#### `feedback()` — Record your own API metrics on-chain
|
|
800
|
+
|
|
801
|
+
Submit `successRate` and `responseTime` after every settled x402 payment. This is what builds the score that `score()` later reads.
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
import Relai from '@relai-fi/x402/server';
|
|
805
|
+
import { score, feedback } from '@relai-fi/x402/plugins';
|
|
806
|
+
|
|
807
|
+
const relai = new Relai({
|
|
808
|
+
network: 'base',
|
|
809
|
+
plugins: [
|
|
810
|
+
score({ agentId: process.env.ERC8004_AGENT_ID! }),
|
|
811
|
+
feedback({ agentId: process.env.ERC8004_AGENT_ID! }),
|
|
812
|
+
],
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Requires `BACKEND_WALLET_PRIVATE_KEY` — the wallet must hold CREDIT tokens on SKALE Base for gas.
|
|
817
|
+
|
|
818
|
+
| Option | Type | Default | Description |
|
|
819
|
+
|--------|------|---------|-------------|
|
|
820
|
+
| `agentId` | `string \| number` | — | Your ERC-8004 agent token ID |
|
|
821
|
+
| `walletPrivateKey` | `string` | `process.env.BACKEND_WALLET_PRIVATE_KEY` | EVM private key, needs CREDIT on SKALE |
|
|
822
|
+
| `rpcUrl` | `string` | `process.env.ERC8004_RPC_URL` | SKALE RPC URL |
|
|
823
|
+
| `reputationRegistryAddress` | `string` | `process.env.ERC8004_REPUTATION_REGISTRY` | Contract address |
|
|
824
|
+
| `submitSuccessRate` | `boolean` | `true` | Submit 1/0 success signal |
|
|
825
|
+
| `submitResponseTime` | `boolean` | `true` | Submit response time in ms |
|
|
826
|
+
|
|
827
|
+
#### `solanaFeedback()` — Native Solana 8004-solana feedback
|
|
828
|
+
|
|
829
|
+
For Solana APIs registered via `8004-solana` (MPL Core NFT). Requires `npm install 8004-solana`.
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
import Relai from '@relai-fi/x402/server';
|
|
833
|
+
import { solanaFeedback } from '@relai-fi/x402/plugins';
|
|
834
|
+
|
|
835
|
+
const relai = new Relai({
|
|
836
|
+
network: 'solana',
|
|
837
|
+
plugins: [
|
|
838
|
+
solanaFeedback({
|
|
839
|
+
assetPubkey: process.env.SOLANA_AGENT_ASSET!, // MPL Core NFT address
|
|
840
|
+
}),
|
|
841
|
+
],
|
|
842
|
+
});
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
| Option | Type | Default | Description |
|
|
846
|
+
|--------|------|---------|-------------|
|
|
847
|
+
| `assetPubkey` | `string` | — | Solana MPL Core NFT address (`solanaAgentAsset`) |
|
|
848
|
+
| `feedbackWalletPrivateKey` | `string` | `process.env.SOLANA_8004_FEEDBACK_KEY` | base58 or JSON array |
|
|
849
|
+
| `cluster` | `'mainnet-beta' \| 'devnet'` | `process.env.SOLANA_8004_CLUSTER` | Solana cluster |
|
|
850
|
+
| `rpcUrl` | `string` | `process.env.SOLANA_8004_RPC_URL` | Custom RPC (Helius / QuickNode) |
|
|
851
|
+
|
|
852
|
+
#### `submitRelayFeedback()` — Third-party feedback utility
|
|
853
|
+
|
|
854
|
+
If your server acts as a **relay or marketplace** that calls other APIs, use this standalone function to record feedback about those APIs. Uses a separate relay wallet to avoid self-feedback restrictions.
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
import { submitRelayFeedback } from '@relai-fi/x402/plugins';
|
|
858
|
+
|
|
859
|
+
// After calling an external API:
|
|
860
|
+
const start = Date.now();
|
|
861
|
+
const result = await fetch('https://other-api.com/v1/data');
|
|
862
|
+
|
|
863
|
+
submitRelayFeedback({
|
|
864
|
+
agentId: '5', // target API's ERC-8004 agentId
|
|
865
|
+
success: result.ok,
|
|
866
|
+
responseTimeMs: Date.now() - start,
|
|
867
|
+
endpoint: '/v1/data',
|
|
868
|
+
});
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
| Option | Type | Default | Description |
|
|
872
|
+
|--------|------|---------|-------------|
|
|
873
|
+
| `agentId` | `string \| number` | — | ERC-8004 agentId of the API you called |
|
|
874
|
+
| `success` | `boolean` | — | Whether the call succeeded |
|
|
875
|
+
| `responseTimeMs` | `number` | `0` | Elapsed time in ms |
|
|
876
|
+
| `endpoint` | `string` | `''` | Endpoint path |
|
|
877
|
+
| `feedbackWalletPrivateKey` | `string` | `process.env.FEEDBACK_WALLET_PRIVATE_KEY` | Must differ from API owner key |
|
|
878
|
+
|
|
879
|
+
#### Required environment variables (ERC-8004)
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
ERC8004_IDENTITY_REGISTRY=0x8724C768547d7fFb1722b13a84F21CCF5010641f
|
|
883
|
+
ERC8004_REPUTATION_REGISTRY=0xe946A7F08d1CC0Ed0eC1fC131D0135d9c0Dd7d9D
|
|
884
|
+
ERC8004_RPC_URL=https://base-sepolia-testnet.skalenodes.com/v1/jubilant-horrible-ancha
|
|
885
|
+
ERC8004_AGENT_ID=5 # your agent NFT token ID
|
|
886
|
+
|
|
887
|
+
BACKEND_WALLET_PRIVATE_KEY=0x... # for feedback() — needs CREDIT on SKALE
|
|
888
|
+
FEEDBACK_WALLET_PRIVATE_KEY=0x... # for submitRelayFeedback() — different wallet
|
|
889
|
+
|
|
890
|
+
# Solana 8004 (only if using solanaFeedback)
|
|
891
|
+
SOLANA_AGENT_ASSET=GH93tGR8... # MPL Core NFT pubkey
|
|
892
|
+
SOLANA_8004_FEEDBACK_KEY=... # base58 or JSON array
|
|
893
|
+
SOLANA_8004_CLUSTER=mainnet-beta
|
|
894
|
+
SOLANA_8004_RPC_URL=https://...
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Combining Plugins
|
|
898
|
+
|
|
899
|
+
Plugins run in array order. Combine them for layered protection:
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
const relai = new Relai({
|
|
903
|
+
network: 'base',
|
|
904
|
+
plugins: [
|
|
905
|
+
shield({ healthUrl: 'https://my-api.com/health' }), // 1. Is the service up?
|
|
906
|
+
preflight({ baseUrl: 'https://my-api.com' }), // 2. Is this endpoint alive?
|
|
907
|
+
circuitBreaker({ failureThreshold: 5 }), // 3. Too many recent failures?
|
|
908
|
+
freeTier({ perBuyerLimit: 5, resetPeriod: 'daily' }), // 4. Free calls left?
|
|
909
|
+
refund({ triggerCodes: [500, 502, 503] }), // 5. Compensate on error
|
|
910
|
+
bridge({ serviceKey: process.env.RELAI_SERVICE_KEY }), // 6. Cross-chain support
|
|
911
|
+
score({ agentId: process.env.ERC8004_AGENT_ID }), // 7. Show reputation in 402
|
|
912
|
+
feedback({ agentId: process.env.ERC8004_AGENT_ID }), // 8. Record metrics on-chain
|
|
913
|
+
],
|
|
914
|
+
});
|
|
915
|
+
```
|
|
578
916
|
|
|
579
917
|
### Custom Plugins
|
|
580
918
|
|
|
581
919
|
```typescript
|
|
582
|
-
import type { RelaiPlugin } from '@relai-fi/x402';
|
|
920
|
+
import type { RelaiPlugin, PluginContext, PluginResult } from '@relai-fi/x402/plugins';
|
|
583
921
|
|
|
584
922
|
const myPlugin: RelaiPlugin = {
|
|
585
|
-
name: '
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
923
|
+
name: 'my-plugin',
|
|
924
|
+
|
|
925
|
+
async beforePaymentCheck(req, ctx) {
|
|
926
|
+
if (req.headers['x-vip'] === 'true') {
|
|
927
|
+
return { skip: true, headers: { 'X-VIP': 'true' } };
|
|
589
928
|
}
|
|
590
929
|
return {};
|
|
591
930
|
},
|
|
592
|
-
};
|
|
593
931
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
932
|
+
async afterSettled(req, result, ctx) {
|
|
933
|
+
console.log(`Paid $${ctx.price} by ${result.payer} on ${ctx.network}`);
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
async onInit() {
|
|
937
|
+
console.log('Plugin initialized');
|
|
938
|
+
},
|
|
939
|
+
};
|
|
598
940
|
```
|
|
599
941
|
|
|
600
942
|
**Plugin interface:**
|
|
@@ -602,9 +944,26 @@ const relai = new Relai({
|
|
|
602
944
|
```typescript
|
|
603
945
|
interface RelaiPlugin {
|
|
604
946
|
name: string;
|
|
605
|
-
beforePaymentCheck
|
|
606
|
-
afterSettled
|
|
607
|
-
onInit
|
|
947
|
+
beforePaymentCheck?(req: any, ctx: PluginContext): Promise<PluginResult>;
|
|
948
|
+
afterSettled?(req: any, result: SettleResult, ctx: PluginContext): Promise<void>;
|
|
949
|
+
onInit?(): Promise<void>;
|
|
950
|
+
enrich402Response?(response: any, ctx: PluginContext): any;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
interface PluginResult {
|
|
954
|
+
skip?: boolean; // Bypass payment, serve content free
|
|
955
|
+
reject?: boolean; // Block request entirely (e.g. unhealthy)
|
|
956
|
+
rejectStatus?: number;
|
|
957
|
+
rejectMessage?: string;
|
|
958
|
+
headers?: Record<string, string>;
|
|
959
|
+
meta?: Record<string, unknown>;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
interface PluginContext {
|
|
963
|
+
network: string;
|
|
964
|
+
price: number;
|
|
965
|
+
path: string;
|
|
966
|
+
method: string;
|
|
608
967
|
}
|
|
609
968
|
```
|
|
610
969
|
|
package/dist/index.cjs
CHANGED
|
@@ -54,6 +54,7 @@ __export(index_exports, {
|
|
|
54
54
|
normalizePaymentHeader: () => normalizePaymentHeader,
|
|
55
55
|
resolveToken: () => resolveToken,
|
|
56
56
|
stripePayTo: () => stripePayTo,
|
|
57
|
+
submitRelayFeedback: () => submitRelayFeedback,
|
|
57
58
|
toAtomicUnits: () => toAtomicUnits
|
|
58
59
|
});
|
|
59
60
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -568,6 +569,9 @@ var Relai = class {
|
|
|
568
569
|
const self = this;
|
|
569
570
|
return async (req, res, next) => {
|
|
570
571
|
try {
|
|
572
|
+
if (req.headers["x-preflight"] === "true" || req.headers["x-preflight"] === "1") {
|
|
573
|
+
return res.status(200).json({ status: "ok", preflight: true });
|
|
574
|
+
}
|
|
571
575
|
const resolvedPrice = typeof options.price === "function" ? await options.price(req) : options.price;
|
|
572
576
|
if (typeof resolvedPrice !== "number" || !isFinite(resolvedPrice) || resolvedPrice <= 0) {
|
|
573
577
|
return res.status(400).json({ error: "Invalid price configuration" });
|
|
@@ -630,6 +634,18 @@ var Relai = class {
|
|
|
630
634
|
if (!plugin.beforePaymentCheck) continue;
|
|
631
635
|
try {
|
|
632
636
|
const pluginResult = await plugin.beforePaymentCheck(req, pluginCtx);
|
|
637
|
+
if (pluginResult?.reject) {
|
|
638
|
+
const rejectStatus = pluginResult.rejectStatus || 503;
|
|
639
|
+
if (pluginResult.headers) {
|
|
640
|
+
for (const [k, v] of Object.entries(pluginResult.headers)) {
|
|
641
|
+
res.setHeader(k, v);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return res.status(rejectStatus).json({
|
|
645
|
+
error: pluginResult.rejectMessage || "Service unavailable",
|
|
646
|
+
plugin: plugin.name
|
|
647
|
+
});
|
|
648
|
+
}
|
|
633
649
|
if (pluginResult?.skip) {
|
|
634
650
|
if (pluginResult.headers) {
|
|
635
651
|
for (const [k, v] of Object.entries(pluginResult.headers)) {
|
|
@@ -844,12 +860,31 @@ var Relai = class {
|
|
|
844
860
|
path: req.path || req.originalUrl || "/",
|
|
845
861
|
method: (req.method || "GET").toUpperCase()
|
|
846
862
|
};
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
863
|
+
const pluginsWithHook = self.plugins.filter((p) => !!p.afterSettled);
|
|
864
|
+
if (pluginsWithHook.length > 0) {
|
|
865
|
+
const originalJson = res.json?.bind(res);
|
|
866
|
+
const originalSend = res.send?.bind(res);
|
|
867
|
+
const fireAfterSettled = (statusCode) => {
|
|
868
|
+
const resultWithStatus = { ...result, statusCode };
|
|
869
|
+
for (const plugin of pluginsWithHook) {
|
|
870
|
+
plugin.afterSettled(req, resultWithStatus, settleCtx).catch((e) => {
|
|
871
|
+
console.warn(`[Relai] Plugin '${plugin.name}' afterSettled error (non-blocking):`, e);
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
if (typeof originalJson === "function") {
|
|
876
|
+
res.json = function(body) {
|
|
877
|
+
fireAfterSettled(res.statusCode ?? 200);
|
|
878
|
+
res.json = originalJson;
|
|
879
|
+
return originalJson(body);
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
if (typeof originalSend === "function") {
|
|
883
|
+
res.send = function(body) {
|
|
884
|
+
fireAfterSettled(res.statusCode ?? 200);
|
|
885
|
+
res.send = originalSend;
|
|
886
|
+
return originalSend(body);
|
|
887
|
+
};
|
|
853
888
|
}
|
|
854
889
|
}
|
|
855
890
|
}
|
|
@@ -1946,6 +1981,64 @@ function createX402Client(config) {
|
|
|
1946
1981
|
return { fetch: x402Fetch };
|
|
1947
1982
|
}
|
|
1948
1983
|
|
|
1984
|
+
// src/relay-feedback.ts
|
|
1985
|
+
var import_ethers = require("ethers");
|
|
1986
|
+
var RELAY_FEEDBACK_REPUTATION_ABI = [
|
|
1987
|
+
"function giveFeedback(uint256 agentId, int128 value, uint8 valueDecimals, string tag1, string tag2, string endpoint, string feedbackURI, bytes32 feedbackHash) external"
|
|
1988
|
+
];
|
|
1989
|
+
function submitRelayFeedback(config) {
|
|
1990
|
+
const agentId = String(config.agentId);
|
|
1991
|
+
const endpoint = config.endpoint ?? "";
|
|
1992
|
+
const responseTimeMs = config.responseTimeMs ?? 0;
|
|
1993
|
+
const privateKey = config.feedbackWalletPrivateKey ?? (typeof process !== "undefined" ? process.env?.FEEDBACK_WALLET_PRIVATE_KEY ?? process.env?.ERC8004_FEEDBACK_WALLET_PRIVATE_KEY : void 0);
|
|
1994
|
+
const reputationAddress = config.reputationRegistryAddress ?? (typeof process !== "undefined" ? process.env?.ERC8004_REPUTATION_REGISTRY : void 0);
|
|
1995
|
+
const rpcUrl = config.rpcUrl ?? (typeof process !== "undefined" ? process.env?.ERC8004_RPC_URL : void 0) ?? "https://base-sepolia-testnet.skalenodes.com/v1/jubilant-horrible-ancha";
|
|
1996
|
+
if (!privateKey || !reputationAddress) {
|
|
1997
|
+
console.warn("[relai:submitRelayFeedback] FEEDBACK_WALLET_PRIVATE_KEY or ERC8004_REPUTATION_REGISTRY not set \u2014 skipping");
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(rpcUrl);
|
|
2001
|
+
const signer = new import_ethers.ethers.Wallet(privateKey, provider);
|
|
2002
|
+
const reputation = new import_ethers.ethers.Contract(reputationAddress, RELAY_FEEDBACK_REPUTATION_ABI, signer);
|
|
2003
|
+
const id = BigInt(agentId);
|
|
2004
|
+
(async () => {
|
|
2005
|
+
const successValue = config.success ? 10000n : 0n;
|
|
2006
|
+
try {
|
|
2007
|
+
const srTx = await reputation.giveFeedback(
|
|
2008
|
+
id,
|
|
2009
|
+
successValue,
|
|
2010
|
+
2,
|
|
2011
|
+
"successRate",
|
|
2012
|
+
"",
|
|
2013
|
+
endpoint,
|
|
2014
|
+
"",
|
|
2015
|
+
import_ethers.ethers.ZeroHash
|
|
2016
|
+
);
|
|
2017
|
+
await srTx.wait();
|
|
2018
|
+
console.log(`[relai:submitRelayFeedback] successRate confirmed agentId=${agentId} success=${config.success}`);
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
console.warn(`[relai:submitRelayFeedback] successRate failed (non-fatal): ${err?.message}`);
|
|
2021
|
+
}
|
|
2022
|
+
if (responseTimeMs > 0) {
|
|
2023
|
+
try {
|
|
2024
|
+
const rtTx = await reputation.giveFeedback(
|
|
2025
|
+
id,
|
|
2026
|
+
BigInt(Math.max(0, Math.round(responseTimeMs))),
|
|
2027
|
+
0,
|
|
2028
|
+
"responseTime",
|
|
2029
|
+
"",
|
|
2030
|
+
endpoint,
|
|
2031
|
+
"",
|
|
2032
|
+
import_ethers.ethers.ZeroHash
|
|
2033
|
+
);
|
|
2034
|
+
console.log(`[relai:submitRelayFeedback] responseTime sent agentId=${agentId} ms=${responseTimeMs} tx=${rtTx.hash}`);
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
console.warn(`[relai:submitRelayFeedback] responseTime failed (non-fatal): ${err?.message}`);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
})();
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1949
2042
|
// src/utils/payload-converter.ts
|
|
1950
2043
|
var NETWORK_V1_TO_V2 = {
|
|
1951
2044
|
"solana": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
@@ -2120,6 +2213,7 @@ function formatUsd(usd, maxDecimals = 4) {
|
|
|
2120
2213
|
normalizePaymentHeader,
|
|
2121
2214
|
resolveToken,
|
|
2122
2215
|
stripePayTo,
|
|
2216
|
+
submitRelayFeedback,
|
|
2123
2217
|
toAtomicUnits
|
|
2124
2218
|
});
|
|
2125
2219
|
//# sourceMappingURL=index.cjs.map
|