@relayos/mcp-paywall 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/README.md +208 -0
- package/dist/agent-wallet.d.ts +41 -0
- package/dist/agent-wallet.d.ts.map +1 -0
- package/dist/agent-wallet.js +146 -0
- package/dist/agent-wallet.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/paywall.d.ts +53 -0
- package/dist/paywall.d.ts.map +1 -0
- package/dist/paywall.js +128 -0
- package/dist/paywall.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/verifier.d.ts +26 -0
- package/dist/verifier.d.ts.map +1 -0
- package/dist/verifier.js +130 -0
- package/dist/verifier.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# @relayos/mcp-paywall
|
|
2
|
+
|
|
3
|
+
> Add pay-per-call RLUSD micropayments to any MCP tool server in one line of code.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@relayos/mcp-paywall)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @relayos/mcp-paywall
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node >= 22. Peer deps: `@modelcontextprotocol/sdk >= 1.0.0`, `zod >= 3.0.0`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Server: Gate any tool behind payment
|
|
22
|
+
|
|
23
|
+
Wrap your existing MCP tool handler with `paywall()`. That's it. No payment infra to run.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
|
+
import { paywall, paywallSchema } from "@relayos/mcp-paywall";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
|
|
30
|
+
const server = new McpServer({ name: "my-data-server", version: "1.0.0" });
|
|
31
|
+
|
|
32
|
+
server.tool(
|
|
33
|
+
"fetch-prices",
|
|
34
|
+
"Fetches proprietary price data",
|
|
35
|
+
paywallSchema({ symbol: z.string() }),
|
|
36
|
+
paywall(
|
|
37
|
+
{
|
|
38
|
+
priceRlusd: 0.10, // $0.10 RLUSD per call
|
|
39
|
+
recipient: "rYourXRPLAddress",
|
|
40
|
+
network: "xrpl_mainnet",
|
|
41
|
+
},
|
|
42
|
+
async ({ symbol }) => ({
|
|
43
|
+
content: [{ type: "text", text: JSON.stringify(await getPrices(symbol)) }],
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `paywallSchema(shape)` — extends your Zod shape with the optional `_relay_payment` field so MCP lets the proof through
|
|
50
|
+
- `paywall(config, handler)` — returns a drop-in replacement handler that enforces payment before execution
|
|
51
|
+
- Agents without a payment proof receive a structured 402 challenge they can parse and auto-pay
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Agent: Auto-pay on 402
|
|
56
|
+
|
|
57
|
+
On the client side, `agentWallet()` intercepts 402 responses, signs an XRPL payment, and retries — transparently.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { agentWallet } from "@relayos/mcp-paywall";
|
|
61
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
62
|
+
|
|
63
|
+
const mcp = new Client({ name: "my-agent", version: "1.0.0" });
|
|
64
|
+
// ... connect mcp to your transport
|
|
65
|
+
|
|
66
|
+
const wallet = agentWallet({
|
|
67
|
+
seed: process.env.AGENT_SEED!, // XRPL wallet seed — held in memory only
|
|
68
|
+
network: "xrpl_mainnet",
|
|
69
|
+
maxSpendPerCallRlusd: 1.0, // hard cap — never pays more than $1 per call
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Transparent auto-pay: call → 402 → sign → retry → result
|
|
73
|
+
const result = await wallet.callWithPayment(
|
|
74
|
+
(name, args) => mcp.callTool({ name, arguments: args }),
|
|
75
|
+
"fetch-prices",
|
|
76
|
+
{ symbol: "BTC" }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
console.log(result.content[0].text);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The agent never pays more than `maxSpendPerCallRlusd`. If the server asks for more, the call throws before signing.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## How it works
|
|
87
|
+
|
|
88
|
+
The 402 handshake follows the [x402 protocol](https://x402.org) adapted for XRPL:
|
|
89
|
+
|
|
90
|
+
- **Challenge** — Server returns `{ error: "PAYMENT_REQUIRED", code: 402, invoice: { priceRlusd, recipient, endpointId, expiresAt } }` when no payment proof is present
|
|
91
|
+
- **Sign** — Agent wallet builds and signs an XRPL RLUSD Payment transaction targeting the exact recipient and amount, encodes it as a base64 proof envelope
|
|
92
|
+
- **Verify & Execute** — Server decodes the proof, checks amount, recipient, expiry, and anti-replay uniqueness, then executes the real handler if valid
|
|
93
|
+
|
|
94
|
+
All verification happens locally on the server — no Relay API call required for the basic flow.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Zero-custody guarantees
|
|
99
|
+
|
|
100
|
+
- **Seed never leaves memory** — `agentWallet()` derives the XRPL address at construction time; the seed string is accessed only at signing time and never stored
|
|
101
|
+
- **Seed is never logged or serialised** — not in error messages, not in network requests
|
|
102
|
+
- **Hard spend cap** — `maxSpendPerCallRlusd` is enforced before any signing; mismatched invoices are rejected, not renegotiated
|
|
103
|
+
- **Anti-replay** — each payment proof is single-use; the server's per-instance store rejects duplicate proofs
|
|
104
|
+
- **Expiry enforcement** — invoices carry an `expiresAt` Unix timestamp; stale proofs are rejected on both sides
|
|
105
|
+
- **No shared state** — each `paywall()` call creates an isolated replay store; multi-tool servers can't cross-contaminate
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## API
|
|
110
|
+
|
|
111
|
+
### `paywall(config, handler)`
|
|
112
|
+
|
|
113
|
+
Wraps an MCP tool handler behind an RLUSD paywall.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
function paywall<P extends Record<string, unknown>>(
|
|
117
|
+
config: PaywallConfig,
|
|
118
|
+
handler: ToolHandler<Omit<P, "_relay_payment">>
|
|
119
|
+
): ToolHandler<P & { _relay_payment?: string }>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**`PaywallConfig`**
|
|
123
|
+
|
|
124
|
+
| Field | Type | Required | Description |
|
|
125
|
+
|-------|------|----------|-------------|
|
|
126
|
+
| `priceRlusd` | `number` | yes | Price in RLUSD per tool call |
|
|
127
|
+
| `recipient` | `string` | yes | XRPL classic address receiving payment |
|
|
128
|
+
| `network` | `"xrpl_mainnet" \| "xrpl_testnet"` | yes | XRPL network |
|
|
129
|
+
| `description` | `string` | no | Human-readable description of what is being sold |
|
|
130
|
+
| `relayApiUrl` | `string` | no | If set, submits the tx to Relay for on-chain settlement confirmation |
|
|
131
|
+
| `gracePeriodMs` | `number` | no | Payment window in ms. Default: `300_000` (5 min) |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### `paywallSchema(shape)`
|
|
136
|
+
|
|
137
|
+
Extends any Zod raw shape with the optional `_relay_payment` field.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
function paywallSchema<T extends ZodRawShape>(
|
|
141
|
+
shape: T
|
|
142
|
+
): T & { _relay_payment: ZodOptional<ZodString> }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use this whenever you declare the tool schema so MCP passes the proof through instead of stripping it as an unknown field.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### `agentWallet(config)`
|
|
150
|
+
|
|
151
|
+
Creates an autonomous XRPL signing wallet for agent-side auto-pay.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
function agentWallet(config: AgentWalletConfig): AgentWallet
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**`AgentWalletConfig`**
|
|
158
|
+
|
|
159
|
+
| Field | Type | Required | Description |
|
|
160
|
+
|-------|------|----------|-------------|
|
|
161
|
+
| `seed` | `string` | yes | XRPL wallet seed. Held in memory only — never logged or transmitted |
|
|
162
|
+
| `network` | `"xrpl_mainnet" \| "xrpl_testnet"` | yes | XRPL network |
|
|
163
|
+
| `maxSpendPerCallRlusd` | `number` | yes | Hard cap per call — agent refuses to pay more than this |
|
|
164
|
+
| `relayApiUrl` | `string` | no | Relay API base URL for server reputation checks before paying |
|
|
165
|
+
| `minServerReputationScore` | `number` | no | Reject servers whose on-chain reputation is below this score |
|
|
166
|
+
|
|
167
|
+
**`AgentWallet`**
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
interface AgentWallet {
|
|
171
|
+
readonly address: string; // XRPL classic address of the agent
|
|
172
|
+
|
|
173
|
+
callWithPayment(
|
|
174
|
+
callTool: (name: string, args: Record<string, unknown>) => Promise<CallToolResult>,
|
|
175
|
+
toolName: string,
|
|
176
|
+
toolArgs: Record<string, unknown>
|
|
177
|
+
): Promise<CallToolResult>;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Types
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// The 402 challenge body returned by a paywalled tool
|
|
187
|
+
interface PaymentInvoice {
|
|
188
|
+
version: "1.0";
|
|
189
|
+
priceRlusd: number;
|
|
190
|
+
recipient: string; // XRPL classic address
|
|
191
|
+
network: Network;
|
|
192
|
+
endpointId: string; // Unique per paywall() registration — prevents cross-tool replays
|
|
193
|
+
expiresAt: number; // Unix timestamp
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Base64-encoded JSON: { scheme, network, payload: signed_tx_blob }
|
|
197
|
+
type PaymentProof = string;
|
|
198
|
+
|
|
199
|
+
type Network = "xrpl_mainnet" | "xrpl_testnet";
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Additional exports: `is402Response`, `extract402Invoice`, `buildInvoice`, `verifyPayment`, `createInMemoryReplayStore` — see [source](https://github.com/timwal78/squeezeos/tree/main/relay/mcp-paywall/src) for full signatures.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT — [timwal78/squeezeos](https://github.com/timwal78/squeezeos)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentWallet() — client-side autonomous XRPL signer.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const wallet = agentWallet({
|
|
6
|
+
* seed: process.env.AGENT_SEED!,
|
|
7
|
+
* network: "xrpl_testnet",
|
|
8
|
+
* maxSpendPerCallRlusd: 1.0,
|
|
9
|
+
* });
|
|
10
|
+
*
|
|
11
|
+
* // Transparent auto-pay: catches 402, pays, retries
|
|
12
|
+
* const result = await wallet.callWithPayment(
|
|
13
|
+
* (name, args) => client.callTool({ name, arguments: args }),
|
|
14
|
+
* "fetch-data",
|
|
15
|
+
* { query: "latest prices" }
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* SECURITY INVARIANTS (enforced by this module):
|
|
19
|
+
* - `seed` is accessed only at call time — never stored after wallet construction
|
|
20
|
+
* - `seed` is never logged, serialised, or included in any network request
|
|
21
|
+
* - Spending is hard-capped at `maxSpendPerCallRlusd` per call
|
|
22
|
+
* - Reputation gate: if `relayApiUrl` + `minServerReputationScore` are set,
|
|
23
|
+
* the server's on-chain score is verified before any payment is signed
|
|
24
|
+
*/
|
|
25
|
+
import type { AgentWalletConfig, CallToolResult } from "./types";
|
|
26
|
+
export type CallToolFn = (name: string, args: Record<string, unknown>) => Promise<CallToolResult>;
|
|
27
|
+
export interface AgentWallet {
|
|
28
|
+
/** The XRPL address of the agent's wallet. */
|
|
29
|
+
readonly address: string;
|
|
30
|
+
/**
|
|
31
|
+
* Call an MCP tool, automatically handling 402 challenges.
|
|
32
|
+
*
|
|
33
|
+
* Flow:
|
|
34
|
+
* 1. Call tool without payment
|
|
35
|
+
* 2. If 402: verify price ≤ limit, optional reputation check, sign + retry
|
|
36
|
+
* 3. If still 402 after retry: throw
|
|
37
|
+
*/
|
|
38
|
+
callWithPayment(callTool: CallToolFn, toolName: string, toolArgs: Record<string, unknown>): Promise<CallToolResult>;
|
|
39
|
+
}
|
|
40
|
+
export declare function agentWallet(config: AgentWalletConfig): AgentWallet;
|
|
41
|
+
//# sourceMappingURL=agent-wallet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-wallet.d.ts","sourceRoot":"","sources":["../src/agent-wallet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EACV,iBAAiB,EAEjB,cAAc,EAEf,MAAM,SAAS,CAAC;AAuFjB,MAAM,MAAM,UAAU,GAAG,CACvB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1B,OAAO,CAAC,cAAc,CAAC,CAAC;AAE7B,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB;;;;;;;OAOG;IACH,eAAe,CACb,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,cAAc,CAAC,CAAC;CAC5B;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,WAAW,CAmElE"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* agentWallet() — client-side autonomous XRPL signer.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* const wallet = agentWallet({
|
|
7
|
+
* seed: process.env.AGENT_SEED!,
|
|
8
|
+
* network: "xrpl_testnet",
|
|
9
|
+
* maxSpendPerCallRlusd: 1.0,
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* // Transparent auto-pay: catches 402, pays, retries
|
|
13
|
+
* const result = await wallet.callWithPayment(
|
|
14
|
+
* (name, args) => client.callTool({ name, arguments: args }),
|
|
15
|
+
* "fetch-data",
|
|
16
|
+
* { query: "latest prices" }
|
|
17
|
+
* );
|
|
18
|
+
*
|
|
19
|
+
* SECURITY INVARIANTS (enforced by this module):
|
|
20
|
+
* - `seed` is accessed only at call time — never stored after wallet construction
|
|
21
|
+
* - `seed` is never logged, serialised, or included in any network request
|
|
22
|
+
* - Spending is hard-capped at `maxSpendPerCallRlusd` per call
|
|
23
|
+
* - Reputation gate: if `relayApiUrl` + `minServerReputationScore` are set,
|
|
24
|
+
* the server's on-chain score is verified before any payment is signed
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.agentWallet = agentWallet;
|
|
28
|
+
const xrpl_1 = require("xrpl");
|
|
29
|
+
const paywall_1 = require("./paywall");
|
|
30
|
+
// ── RLUSD issuers ─────────────────────────────────────────────────────────────
|
|
31
|
+
const RLUSD_ISSUERS = {
|
|
32
|
+
xrpl_mainnet: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
|
33
|
+
xrpl_testnet: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
|
|
34
|
+
};
|
|
35
|
+
const XRPL_NODES = {
|
|
36
|
+
xrpl_mainnet: "wss://xrplcluster.com",
|
|
37
|
+
xrpl_testnet: "wss://s.altnet.rippletest.net:51233",
|
|
38
|
+
};
|
|
39
|
+
// ── Signing ───────────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Build a signed RLUSD Payment tx blob for the given invoice.
|
|
42
|
+
*
|
|
43
|
+
* Connects to XRPL to autofill sequence + fee.
|
|
44
|
+
* For test injection, provide `config._signPayment` to bypass network calls.
|
|
45
|
+
*/
|
|
46
|
+
async function buildPaymentProof(invoice, config) {
|
|
47
|
+
// Test injection path — zero network access
|
|
48
|
+
if (config._signPayment) {
|
|
49
|
+
const txBlob = await config._signPayment(invoice);
|
|
50
|
+
return buildProofEnvelope(txBlob, config.network);
|
|
51
|
+
}
|
|
52
|
+
const wallet = xrpl_1.Wallet.fromSeed(config.seed);
|
|
53
|
+
const xrpl = new xrpl_1.Client(XRPL_NODES[config.network]);
|
|
54
|
+
await xrpl.connect();
|
|
55
|
+
try {
|
|
56
|
+
const tx = {
|
|
57
|
+
TransactionType: "Payment",
|
|
58
|
+
Account: wallet.classicAddress,
|
|
59
|
+
Destination: invoice.recipient,
|
|
60
|
+
Amount: {
|
|
61
|
+
currency: "USD",
|
|
62
|
+
issuer: RLUSD_ISSUERS[config.network],
|
|
63
|
+
value: invoice.priceRlusd.toString(),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const prepared = await xrpl.autofill(tx);
|
|
67
|
+
const { tx_blob } = wallet.sign(prepared);
|
|
68
|
+
return buildProofEnvelope(tx_blob, config.network);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await xrpl.disconnect();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function buildProofEnvelope(txBlob, network) {
|
|
75
|
+
const envelope = {
|
|
76
|
+
scheme: "exact",
|
|
77
|
+
network: network === "xrpl_mainnet" ? "xrpl-mainnet" : "xrpl-testnet",
|
|
78
|
+
payload: txBlob,
|
|
79
|
+
};
|
|
80
|
+
return Buffer.from(JSON.stringify(envelope)).toString("base64");
|
|
81
|
+
}
|
|
82
|
+
// ── Reputation gate ───────────────────────────────────────────────────────────
|
|
83
|
+
async function checkServerReputation(recipient, relayApiUrl, minScore) {
|
|
84
|
+
try {
|
|
85
|
+
const url = `${relayApiUrl.replace(/\/$/, "")}/api/v1/reputation/${recipient}`;
|
|
86
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
return { safe: true, score: 0 }; // graceful degradation on error
|
|
89
|
+
const data = (await res.json());
|
|
90
|
+
const score = data?.score?.score ?? 0;
|
|
91
|
+
return { safe: score >= minScore, score };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { safe: true, score: 0 }; // never block on reputation fetch failure
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function agentWallet(config) {
|
|
98
|
+
// Derive address without storing seed reference beyond this call
|
|
99
|
+
const address = xrpl_1.Wallet.fromSeed(config.seed).classicAddress;
|
|
100
|
+
return {
|
|
101
|
+
address,
|
|
102
|
+
async callWithPayment(callTool, toolName, toolArgs) {
|
|
103
|
+
// Attempt 1 — no payment
|
|
104
|
+
const first = await callTool(toolName, toolArgs);
|
|
105
|
+
if (!(0, paywall_1.is402Response)(first))
|
|
106
|
+
return first;
|
|
107
|
+
// Extract challenge
|
|
108
|
+
const invoice = (0, paywall_1.extract402Invoice)(first);
|
|
109
|
+
if (!invoice) {
|
|
110
|
+
throw new Error("Received 402 but could not parse payment invoice");
|
|
111
|
+
}
|
|
112
|
+
// Spending guard — hard cap per call
|
|
113
|
+
if (invoice.priceRlusd > config.maxSpendPerCallRlusd) {
|
|
114
|
+
throw new Error(`Tool "${toolName}" costs ${invoice.priceRlusd} RLUSD which exceeds ` +
|
|
115
|
+
`maxSpendPerCallRlusd limit of ${config.maxSpendPerCallRlusd} RLUSD`);
|
|
116
|
+
}
|
|
117
|
+
// Invoice expiry check
|
|
118
|
+
if (invoice.expiresAt < Math.floor(Date.now() / 1000)) {
|
|
119
|
+
throw new Error(`Payment invoice for "${toolName}" has expired`);
|
|
120
|
+
}
|
|
121
|
+
// Optional: verify server reputation before paying
|
|
122
|
+
if (config.relayApiUrl && config.minServerReputationScore) {
|
|
123
|
+
const { safe, score } = await checkServerReputation(invoice.recipient, config.relayApiUrl, config.minServerReputationScore);
|
|
124
|
+
if (!safe) {
|
|
125
|
+
throw new Error(`Server "${invoice.recipient}" reputation score ${score} is below ` +
|
|
126
|
+
`minimum required ${config.minServerReputationScore} — payment refused`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Sign payment
|
|
130
|
+
const proof = await buildPaymentProof(invoice, config);
|
|
131
|
+
// Attempt 2 — with payment proof
|
|
132
|
+
const second = await callTool(toolName, {
|
|
133
|
+
...toolArgs,
|
|
134
|
+
_relay_payment: proof,
|
|
135
|
+
});
|
|
136
|
+
if ((0, paywall_1.is402Response)(second)) {
|
|
137
|
+
const fc = second.content[0];
|
|
138
|
+
const rejText = (fc?.type === "text" ? fc.text : undefined) ?? "{}";
|
|
139
|
+
const rejection = JSON.parse(rejText);
|
|
140
|
+
throw new Error(`Payment rejected by server: ${rejection.reason ?? "unknown reason"}`);
|
|
141
|
+
}
|
|
142
|
+
return second;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=agent-wallet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-wallet.js","sourceRoot":"","sources":["../src/agent-wallet.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @relayos/mcp-paywall — x402 RLUSD payment layer for Model Context Protocol.
|
|
3
|
+
*
|
|
4
|
+
* Server (earning wedge):
|
|
5
|
+
* import { paywall, paywallSchema } from "@relayos/mcp-paywall";
|
|
6
|
+
*
|
|
7
|
+
* Client (spending wedge):
|
|
8
|
+
* import { agentWallet } from "@relayos/mcp-paywall";
|
|
9
|
+
*/
|
|
10
|
+
export { paywall, paywallSchema, is402Response, extract402Invoice, buildInvoice } from "./paywall";
|
|
11
|
+
export { agentWallet } from "./agent-wallet";
|
|
12
|
+
export { verifyPayment, createInMemoryReplayStore } from "./verifier";
|
|
13
|
+
export type { PaywallConfig, AgentWalletConfig, PaymentInvoice, PaymentChallenge, PaymentProof, CallToolResult, ToolHandler, ToolContent, TextContent, ImageContent, VerificationResult, Network, } from "./types";
|
|
14
|
+
export type { AntiReplayStore } from "./verifier";
|
|
15
|
+
export type { AgentWallet, CallToolFn } from "./agent-wallet";
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACnG,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AACtE,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,WAAW,EACX,WAAW,EACX,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,OAAO,GACR,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @relayos/mcp-paywall — x402 RLUSD payment layer for Model Context Protocol.
|
|
4
|
+
*
|
|
5
|
+
* Server (earning wedge):
|
|
6
|
+
* import { paywall, paywallSchema } from "@relayos/mcp-paywall";
|
|
7
|
+
*
|
|
8
|
+
* Client (spending wedge):
|
|
9
|
+
* import { agentWallet } from "@relayos/mcp-paywall";
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createInMemoryReplayStore = exports.verifyPayment = exports.agentWallet = exports.buildInvoice = exports.extract402Invoice = exports.is402Response = exports.paywallSchema = exports.paywall = void 0;
|
|
13
|
+
var paywall_1 = require("./paywall");
|
|
14
|
+
Object.defineProperty(exports, "paywall", { enumerable: true, get: function () { return paywall_1.paywall; } });
|
|
15
|
+
Object.defineProperty(exports, "paywallSchema", { enumerable: true, get: function () { return paywall_1.paywallSchema; } });
|
|
16
|
+
Object.defineProperty(exports, "is402Response", { enumerable: true, get: function () { return paywall_1.is402Response; } });
|
|
17
|
+
Object.defineProperty(exports, "extract402Invoice", { enumerable: true, get: function () { return paywall_1.extract402Invoice; } });
|
|
18
|
+
Object.defineProperty(exports, "buildInvoice", { enumerable: true, get: function () { return paywall_1.buildInvoice; } });
|
|
19
|
+
var agent_wallet_1 = require("./agent-wallet");
|
|
20
|
+
Object.defineProperty(exports, "agentWallet", { enumerable: true, get: function () { return agent_wallet_1.agentWallet; } });
|
|
21
|
+
var verifier_1 = require("./verifier");
|
|
22
|
+
Object.defineProperty(exports, "verifyPayment", { enumerable: true, get: function () { return verifier_1.verifyPayment; } });
|
|
23
|
+
Object.defineProperty(exports, "createInMemoryReplayStore", { enumerable: true, get: function () { return verifier_1.createInMemoryReplayStore; } });
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAEH,qCAAmG;AAA1F,kGAAA,OAAO,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,4GAAA,iBAAiB,OAAA;AAAE,uGAAA,YAAY,OAAA;AAC/E,+CAA6C;AAApC,2GAAA,WAAW,OAAA;AACpB,uCAAsE;AAA7D,yGAAA,aAAa,OAAA;AAAE,qHAAA,yBAAyB,OAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paywall() — server-side MCP tool wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { paywall, paywallSchema } from "@relayos/mcp-paywall";
|
|
6
|
+
* import { z } from "zod";
|
|
7
|
+
*
|
|
8
|
+
* server.tool(
|
|
9
|
+
* "fetch-data",
|
|
10
|
+
* "Fetches proprietary data",
|
|
11
|
+
* paywallSchema({ query: z.string() }),
|
|
12
|
+
* paywall(
|
|
13
|
+
* { priceRlusd: 0.10, recipient: "rYourAddress", network: "xrpl_testnet" },
|
|
14
|
+
* async ({ query }) => ({ content: [{ type: "text", text: yourData(query) }] })
|
|
15
|
+
* )
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* When a tool call arrives without `_relay_payment`, the wrapper returns a
|
|
19
|
+
* structured 402 challenge the agent wallet can parse and auto-pay.
|
|
20
|
+
* After a valid payment proof is provided the real handler executes — with
|
|
21
|
+
* `_relay_payment` stripped from params so the inner handler stays clean.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import type { PaywallConfig, ToolHandler, CallToolResult, PaymentInvoice } from "./types";
|
|
25
|
+
/**
|
|
26
|
+
* Extend any Zod raw shape with the optional `_relay_payment` field.
|
|
27
|
+
*
|
|
28
|
+
* MCP validates tool arguments against the declared schema; additional fields
|
|
29
|
+
* are stripped. Call this wrapper around your schema to let the payment proof
|
|
30
|
+
* pass through to the handler.
|
|
31
|
+
*
|
|
32
|
+
* server.tool("name", paywallSchema({ foo: z.string() }), paywall(cfg, handler))
|
|
33
|
+
*/
|
|
34
|
+
export declare function paywallSchema<T extends z.ZodRawShape>(schema: T): T & {
|
|
35
|
+
_relay_payment: z.ZodOptional<z.ZodString>;
|
|
36
|
+
};
|
|
37
|
+
export declare function buildInvoice(config: PaywallConfig, endpointId: string): PaymentInvoice;
|
|
38
|
+
/**
|
|
39
|
+
* Wrap any MCP tool handler behind an x402 RLUSD paywall.
|
|
40
|
+
*
|
|
41
|
+
* Returns a handler function that:
|
|
42
|
+
* 1. Returns a 402 challenge if `_relay_payment` is absent
|
|
43
|
+
* 2. Verifies the proof (amount, recipient, anti-replay)
|
|
44
|
+
* 3. Calls the real handler with `_relay_payment` stripped from params
|
|
45
|
+
*/
|
|
46
|
+
export declare function paywall<P extends Record<string, unknown>>(config: PaywallConfig, handler: ToolHandler<Omit<P, "_relay_payment">>): ToolHandler<P & {
|
|
47
|
+
_relay_payment?: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** Detect whether a CallToolResult carries a Relay 402 challenge. */
|
|
50
|
+
export declare function is402Response(result: CallToolResult): boolean;
|
|
51
|
+
/** Extract the PaymentInvoice from a 402 CallToolResult. Returns null if not a 402. */
|
|
52
|
+
export declare function extract402Invoice(result: CallToolResult): PaymentInvoice | null;
|
|
53
|
+
//# sourceMappingURL=paywall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../src/paywall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EACV,aAAa,EACb,WAAW,EACX,cAAc,EAEd,cAAc,EACf,MAAM,SAAS,CAAC;AAIjB;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EACnD,MAAM,EAAE,CAAC,GACR,CAAC,GAAG;IAAE,cAAc,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;CAAE,CAEpD;AAID,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAStF;AAuBD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvD,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,GAC9C,WAAW,CAAC,CAAC,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAoB9C;AAID,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAU7D;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,GAAG,IAAI,CAU/E"}
|
package/dist/paywall.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* paywall() — server-side MCP tool wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { paywall, paywallSchema } from "@relayos/mcp-paywall";
|
|
7
|
+
* import { z } from "zod";
|
|
8
|
+
*
|
|
9
|
+
* server.tool(
|
|
10
|
+
* "fetch-data",
|
|
11
|
+
* "Fetches proprietary data",
|
|
12
|
+
* paywallSchema({ query: z.string() }),
|
|
13
|
+
* paywall(
|
|
14
|
+
* { priceRlusd: 0.10, recipient: "rYourAddress", network: "xrpl_testnet" },
|
|
15
|
+
* async ({ query }) => ({ content: [{ type: "text", text: yourData(query) }] })
|
|
16
|
+
* )
|
|
17
|
+
* );
|
|
18
|
+
*
|
|
19
|
+
* When a tool call arrives without `_relay_payment`, the wrapper returns a
|
|
20
|
+
* structured 402 challenge the agent wallet can parse and auto-pay.
|
|
21
|
+
* After a valid payment proof is provided the real handler executes — with
|
|
22
|
+
* `_relay_payment` stripped from params so the inner handler stays clean.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.paywallSchema = paywallSchema;
|
|
26
|
+
exports.buildInvoice = buildInvoice;
|
|
27
|
+
exports.paywall = paywall;
|
|
28
|
+
exports.is402Response = is402Response;
|
|
29
|
+
exports.extract402Invoice = extract402Invoice;
|
|
30
|
+
const zod_1 = require("zod");
|
|
31
|
+
const verifier_1 = require("./verifier");
|
|
32
|
+
// ── paywallSchema ─────────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Extend any Zod raw shape with the optional `_relay_payment` field.
|
|
35
|
+
*
|
|
36
|
+
* MCP validates tool arguments against the declared schema; additional fields
|
|
37
|
+
* are stripped. Call this wrapper around your schema to let the payment proof
|
|
38
|
+
* pass through to the handler.
|
|
39
|
+
*
|
|
40
|
+
* server.tool("name", paywallSchema({ foo: z.string() }), paywall(cfg, handler))
|
|
41
|
+
*/
|
|
42
|
+
function paywallSchema(schema) {
|
|
43
|
+
return { ...schema, _relay_payment: zod_1.z.string().optional() };
|
|
44
|
+
}
|
|
45
|
+
// ── Challenge builder ─────────────────────────────────────────────────────────
|
|
46
|
+
function buildInvoice(config, endpointId) {
|
|
47
|
+
return {
|
|
48
|
+
version: "1.0",
|
|
49
|
+
priceRlusd: config.priceRlusd,
|
|
50
|
+
recipient: config.recipient,
|
|
51
|
+
network: config.network,
|
|
52
|
+
endpointId,
|
|
53
|
+
expiresAt: Math.floor(Date.now() / 1000) + Math.floor((config.gracePeriodMs ?? 300_000) / 1000),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function challengeResult(config, endpointId) {
|
|
57
|
+
const challenge = {
|
|
58
|
+
error: "PAYMENT_REQUIRED",
|
|
59
|
+
code: 402,
|
|
60
|
+
invoice: buildInvoice(config, endpointId),
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: JSON.stringify(challenge) }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function rejectionResult(reason) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: JSON.stringify({ error: "PAYMENT_INVALID", code: 402, reason }) }],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ── paywall ───────────────────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Wrap any MCP tool handler behind an x402 RLUSD paywall.
|
|
76
|
+
*
|
|
77
|
+
* Returns a handler function that:
|
|
78
|
+
* 1. Returns a 402 challenge if `_relay_payment` is absent
|
|
79
|
+
* 2. Verifies the proof (amount, recipient, anti-replay)
|
|
80
|
+
* 3. Calls the real handler with `_relay_payment` stripped from params
|
|
81
|
+
*/
|
|
82
|
+
function paywall(config, handler) {
|
|
83
|
+
const endpointId = `${config.recipient}:${config.priceRlusd}:${config.network}`;
|
|
84
|
+
// Per-instance store: each paywall() call is isolated from every other.
|
|
85
|
+
// For multi-replica deployments swap this for a Redis-backed store.
|
|
86
|
+
const store = (0, verifier_1.createInMemoryReplayStore)();
|
|
87
|
+
return async (params, extra) => {
|
|
88
|
+
const { _relay_payment, ...toolParams } = params;
|
|
89
|
+
if (!_relay_payment || typeof _relay_payment !== "string") {
|
|
90
|
+
return challengeResult(config, endpointId);
|
|
91
|
+
}
|
|
92
|
+
const result = await (0, verifier_1.verifyPayment)(_relay_payment, config, store);
|
|
93
|
+
if (!result.valid) {
|
|
94
|
+
return rejectionResult(result.reason ?? "Payment verification failed");
|
|
95
|
+
}
|
|
96
|
+
return handler(toolParams, extra);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── Type guard ────────────────────────────────────────────────────────────────
|
|
100
|
+
/** Detect whether a CallToolResult carries a Relay 402 challenge. */
|
|
101
|
+
function is402Response(result) {
|
|
102
|
+
if (!result.isError)
|
|
103
|
+
return false;
|
|
104
|
+
try {
|
|
105
|
+
const first = result.content[0];
|
|
106
|
+
const text = (first?.type === "text" ? first.text : undefined) ?? "";
|
|
107
|
+
const parsed = JSON.parse(text);
|
|
108
|
+
return parsed.code === 402;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Extract the PaymentInvoice from a 402 CallToolResult. Returns null if not a 402. */
|
|
115
|
+
function extract402Invoice(result) {
|
|
116
|
+
try {
|
|
117
|
+
const first = result.content[0];
|
|
118
|
+
const text = (first?.type === "text" ? first.text : undefined) ?? "";
|
|
119
|
+
const parsed = JSON.parse(text);
|
|
120
|
+
if (parsed.code === 402 && parsed.invoice)
|
|
121
|
+
return parsed.invoice;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// fall through
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=paywall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paywall.js","sourceRoot":"","sources":["../src/paywall.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for @relayos/mcp-paywall.
|
|
3
|
+
* Self-contained — no imports from relay/sdk to keep the package standalone.
|
|
4
|
+
*/
|
|
5
|
+
export type Network = "xrpl_mainnet" | "xrpl_testnet";
|
|
6
|
+
export interface PaymentInvoice {
|
|
7
|
+
/** Version tag for future protocol upgrades. */
|
|
8
|
+
version: "1.0";
|
|
9
|
+
priceRlusd: number;
|
|
10
|
+
recipient: string;
|
|
11
|
+
network: Network;
|
|
12
|
+
/** Unique per tool registration — prevents cross-tool replays. */
|
|
13
|
+
endpointId: string;
|
|
14
|
+
/** Unix timestamp. Client must pay before this. */
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface PaymentChallenge {
|
|
18
|
+
error: "PAYMENT_REQUIRED";
|
|
19
|
+
code: 402;
|
|
20
|
+
invoice: PaymentInvoice;
|
|
21
|
+
}
|
|
22
|
+
/** Base64-encoded JSON: { scheme, network, payload: signed_tx_blob } */
|
|
23
|
+
export type PaymentProof = string;
|
|
24
|
+
export interface PaywallConfig {
|
|
25
|
+
/** Price in RLUSD for one tool call. */
|
|
26
|
+
priceRlusd: number;
|
|
27
|
+
/** XRPL address that receives payment. */
|
|
28
|
+
recipient: string;
|
|
29
|
+
network: Network;
|
|
30
|
+
description?: string;
|
|
31
|
+
/** If set, server submits the tx to XRPL for settlement confirmation. */
|
|
32
|
+
relayApiUrl?: string;
|
|
33
|
+
/** Payment window in ms. Default: 300_000 (5 min). */
|
|
34
|
+
gracePeriodMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface AgentWalletConfig {
|
|
37
|
+
/** XRPL wallet seed. Held in memory only — NEVER logged or transmitted. */
|
|
38
|
+
seed: string;
|
|
39
|
+
network: Network;
|
|
40
|
+
/** Maximum price per tool call the agent will auto-pay without human approval. */
|
|
41
|
+
maxSpendPerCallRlusd: number;
|
|
42
|
+
/** Relay API base URL for reputation checks before paying. */
|
|
43
|
+
relayApiUrl?: string;
|
|
44
|
+
/** Reject servers whose reputation score is below this threshold. */
|
|
45
|
+
minServerReputationScore?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Inject a custom signer for testing.
|
|
48
|
+
* Production code omits this — real XRPL signing is used.
|
|
49
|
+
*/
|
|
50
|
+
_signPayment?: (invoice: PaymentInvoice) => Promise<string>;
|
|
51
|
+
}
|
|
52
|
+
export interface TextContent {
|
|
53
|
+
type: "text";
|
|
54
|
+
text: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ImageContent {
|
|
57
|
+
type: "image";
|
|
58
|
+
data: string;
|
|
59
|
+
mimeType: string;
|
|
60
|
+
}
|
|
61
|
+
export type ToolContent = TextContent | ImageContent;
|
|
62
|
+
export interface CallToolResult {
|
|
63
|
+
content: ToolContent[];
|
|
64
|
+
isError?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export type ToolHandler<P extends Record<string, unknown> = Record<string, unknown>> = (params: P, extra?: unknown) => CallToolResult | Promise<CallToolResult>;
|
|
67
|
+
export interface VerificationResult {
|
|
68
|
+
valid: boolean;
|
|
69
|
+
reason?: string;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,cAAc,CAAC;AAItD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,OAAO,EAAE,KAAK,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,kBAAkB,CAAC;IAC1B,IAAI,EAAE,GAAG,CAAC;IACV,OAAO,EAAE,cAAc,CAAC;CACzB;AAID,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAIlC,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,oBAAoB,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7D;AAID,MAAM,WAAW,WAAW;IAAI,IAAI,EAAE,MAAM,CAAC;IAAK,IAAI,EAAE,MAAM,CAAA;CAAE;AAChE,MAAM,WAAW,YAAY;IAAG,IAAI,EAAE,OAAO,CAAC;IAAI,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAElF,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;AAErD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACjF,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;AAI3E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;GAGG"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment proof verification.
|
|
3
|
+
*
|
|
4
|
+
* The proof is a base64-encoded JSON envelope:
|
|
5
|
+
* { scheme: "exact", network: string, payload: "<signed_xrpl_tx_blob>" }
|
|
6
|
+
*
|
|
7
|
+
* Verification steps:
|
|
8
|
+
* 1. Decode the envelope
|
|
9
|
+
* 2. Decode the XRPL tx blob (xrpl.decode)
|
|
10
|
+
* 3. Verify TransactionType === "Payment"
|
|
11
|
+
* 4. Verify Destination === config.recipient
|
|
12
|
+
* 5. Verify Amount >= config.priceRlusd (RLUSD IOU or XRP)
|
|
13
|
+
* 6. Anti-replay: reject if this tx_blob was used in the last gracePeriodMs
|
|
14
|
+
*
|
|
15
|
+
* Anti-replay is in-process (Map with TTL). For multi-instance deployments
|
|
16
|
+
* replace with a shared Redis SET — the `_antiReplay` export is injectable.
|
|
17
|
+
*/
|
|
18
|
+
import type { PaywallConfig, VerificationResult } from "./types";
|
|
19
|
+
export interface AntiReplayStore {
|
|
20
|
+
has(key: string): boolean;
|
|
21
|
+
set(key: string, expiresAt: number): void;
|
|
22
|
+
sweep(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function createInMemoryReplayStore(): AntiReplayStore;
|
|
25
|
+
export declare function verifyPayment(proofBase64: string, config: PaywallConfig, store?: AntiReplayStore): Promise<VerificationResult>;
|
|
26
|
+
//# sourceMappingURL=verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../src/verifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAIjE,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,yBAAyB,IAAI,eAAe,CAe3D;AAcD,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,KAAK,GAAE,eAA+B,GACrC,OAAO,CAAC,kBAAkB,CAAC,CAiD7B"}
|
package/dist/verifier.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Payment proof verification.
|
|
4
|
+
*
|
|
5
|
+
* The proof is a base64-encoded JSON envelope:
|
|
6
|
+
* { scheme: "exact", network: string, payload: "<signed_xrpl_tx_blob>" }
|
|
7
|
+
*
|
|
8
|
+
* Verification steps:
|
|
9
|
+
* 1. Decode the envelope
|
|
10
|
+
* 2. Decode the XRPL tx blob (xrpl.decode)
|
|
11
|
+
* 3. Verify TransactionType === "Payment"
|
|
12
|
+
* 4. Verify Destination === config.recipient
|
|
13
|
+
* 5. Verify Amount >= config.priceRlusd (RLUSD IOU or XRP)
|
|
14
|
+
* 6. Anti-replay: reject if this tx_blob was used in the last gracePeriodMs
|
|
15
|
+
*
|
|
16
|
+
* Anti-replay is in-process (Map with TTL). For multi-instance deployments
|
|
17
|
+
* replace with a shared Redis SET — the `_antiReplay` export is injectable.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.createInMemoryReplayStore = createInMemoryReplayStore;
|
|
21
|
+
exports.verifyPayment = verifyPayment;
|
|
22
|
+
const xrpl_1 = require("xrpl");
|
|
23
|
+
function createInMemoryReplayStore() {
|
|
24
|
+
const store = new Map();
|
|
25
|
+
return {
|
|
26
|
+
has(key) {
|
|
27
|
+
const exp = store.get(key);
|
|
28
|
+
if (exp === undefined)
|
|
29
|
+
return false;
|
|
30
|
+
if (Date.now() > exp) {
|
|
31
|
+
store.delete(key);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
set(key, expiresAt) { store.set(key, expiresAt); },
|
|
37
|
+
sweep() {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
for (const [k, exp] of store)
|
|
40
|
+
if (now > exp)
|
|
41
|
+
store.delete(k);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Default store — per-process singleton
|
|
46
|
+
const _defaultStore = createInMemoryReplayStore();
|
|
47
|
+
// ── RLUSD issuer addresses ────────────────────────────────────────────────────
|
|
48
|
+
const RLUSD_ISSUERS = {
|
|
49
|
+
xrpl_mainnet: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
|
50
|
+
xrpl_testnet: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
|
|
51
|
+
};
|
|
52
|
+
// ── Core verification ─────────────────────────────────────────────────────────
|
|
53
|
+
async function verifyPayment(proofBase64, config, store = _defaultStore) {
|
|
54
|
+
// 1. Parse outer envelope
|
|
55
|
+
let envelope;
|
|
56
|
+
try {
|
|
57
|
+
envelope = JSON.parse(Buffer.from(proofBase64, "base64").toString("utf8"));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { valid: false, reason: "Malformed proof envelope" };
|
|
61
|
+
}
|
|
62
|
+
const txBlob = envelope.payload;
|
|
63
|
+
if (typeof txBlob !== "string" || !txBlob) {
|
|
64
|
+
return { valid: false, reason: "Missing tx payload in proof" };
|
|
65
|
+
}
|
|
66
|
+
// 2. Decode XRPL tx blob
|
|
67
|
+
let decoded;
|
|
68
|
+
try {
|
|
69
|
+
decoded = (0, xrpl_1.decode)(txBlob);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { valid: false, reason: "Cannot decode XRPL tx blob" };
|
|
73
|
+
}
|
|
74
|
+
// 3. Transaction type
|
|
75
|
+
if (decoded.TransactionType !== "Payment") {
|
|
76
|
+
return { valid: false, reason: `Expected Payment tx, got ${decoded.TransactionType}` };
|
|
77
|
+
}
|
|
78
|
+
// 4. Destination
|
|
79
|
+
if (decoded.Destination !== config.recipient) {
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
reason: `Wrong recipient: expected ${config.recipient}, got ${decoded.Destination}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// 5. Amount: RLUSD IOU or XRP drops
|
|
86
|
+
const ok = verifyAmount(decoded.Amount, config.priceRlusd, config.network);
|
|
87
|
+
if (!ok.valid)
|
|
88
|
+
return ok;
|
|
89
|
+
// 6. Anti-replay: use tx_blob fingerprint (last 32 chars of blob are signature-unique)
|
|
90
|
+
const replayKey = `${decoded.Destination}:${txBlob.slice(-48)}`;
|
|
91
|
+
if (store.has(replayKey)) {
|
|
92
|
+
return { valid: false, reason: "Payment proof already used (replay attack)" };
|
|
93
|
+
}
|
|
94
|
+
const gracePeriodMs = config.gracePeriodMs ?? 300_000;
|
|
95
|
+
store.set(replayKey, Date.now() + gracePeriodMs);
|
|
96
|
+
return { valid: true };
|
|
97
|
+
}
|
|
98
|
+
function verifyAmount(amount, requiredRlusd, network) {
|
|
99
|
+
if (amount === null || amount === undefined) {
|
|
100
|
+
return { valid: false, reason: "Missing Amount field" };
|
|
101
|
+
}
|
|
102
|
+
// RLUSD IOU: { currency: "USD", issuer: "...", value: "0.10" }
|
|
103
|
+
if (typeof amount === "object") {
|
|
104
|
+
const iou = amount;
|
|
105
|
+
if (iou.currency !== "USD") {
|
|
106
|
+
return { valid: false, reason: `Expected USD currency, got ${iou.currency}` };
|
|
107
|
+
}
|
|
108
|
+
const expectedIssuer = RLUSD_ISSUERS[network];
|
|
109
|
+
if (expectedIssuer && iou.issuer !== expectedIssuer) {
|
|
110
|
+
return { valid: false, reason: "RLUSD issuer mismatch" };
|
|
111
|
+
}
|
|
112
|
+
const paid = parseFloat(iou.value ?? "0");
|
|
113
|
+
if (paid < requiredRlusd) {
|
|
114
|
+
return { valid: false, reason: `Underpayment: ${paid} RLUSD < ${requiredRlusd} RLUSD required` };
|
|
115
|
+
}
|
|
116
|
+
return { valid: true };
|
|
117
|
+
}
|
|
118
|
+
// XRP drops (fallback; servers should prefer RLUSD)
|
|
119
|
+
if (typeof amount === "string") {
|
|
120
|
+
const drops = parseInt(amount, 10);
|
|
121
|
+
const xrp = drops / 1_000_000;
|
|
122
|
+
// Testnet approximation: 1 RLUSD = 0.5 XRP — accept if 2x overcharged to be safe
|
|
123
|
+
if (xrp < requiredRlusd * 0.4) {
|
|
124
|
+
return { valid: false, reason: `XRP amount ${xrp} insufficient for ${requiredRlusd} RLUSD` };
|
|
125
|
+
}
|
|
126
|
+
return { valid: true };
|
|
127
|
+
}
|
|
128
|
+
return { valid: false, reason: "Unrecognised Amount format" };
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=verifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifier.js","sourceRoot":"","sources":["../src/verifier.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@relayos/mcp-paywall",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-friction x402 RLUSD payment layer for Model Context Protocol tools",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": ["dist", "README.md"],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/timwal78/squeezeos"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"xrpl",
|
|
21
|
+
"rlusd",
|
|
22
|
+
"x402",
|
|
23
|
+
"agent-payments",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"ai-payments",
|
|
26
|
+
"micropayments"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://github.com/timwal78/squeezeos/tree/main/relay/mcp-paywall",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"prepublishOnly": "npm run build && npm test",
|
|
32
|
+
"test": "jest --no-coverage",
|
|
33
|
+
"dev": "tsc --watch"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": ">=1.0.0",
|
|
37
|
+
"zod": ">=3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"xrpl": "^4.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
|
+
"@types/jest": "^29.5.0",
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"jest": "^29.7.0",
|
|
47
|
+
"ts-jest": "^29.2.0",
|
|
48
|
+
"typescript": "^5.4.0",
|
|
49
|
+
"zod": "^3.22.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": { "node": ">=22.0.0" },
|
|
52
|
+
"publishConfig": { "access": "public" },
|
|
53
|
+
"license": "MIT"
|
|
54
|
+
}
|