@cloudpayx/x402-pay 0.2.4 → 0.2.5

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.
@@ -0,0 +1,60 @@
1
+ import { createServer } from "node:http";
2
+ import { Readable } from "node:stream";
3
+ import { createCpXAgentEarn } from "@cloudpayx/x402-pay";
4
+
5
+ const address = process.env.XRPL_RECEIVING_ADDRESS;
6
+ const facilitator = process.env.X402_FACILITATOR_URL;
7
+
8
+ if (!address || !facilitator) {
9
+ console.error("Set XRPL_RECEIVING_ADDRESS and X402_FACILITATOR_URL.");
10
+ process.exitCode = 1;
11
+ } else {
12
+ const earn = createCpXAgentEarn({
13
+ walletAddress: address,
14
+ asset: process.env.CPX_ASSET ?? "XRP",
15
+ amount: process.env.CPX_AMOUNT ?? "1000",
16
+ invoiceId: process.env.CPX_INVOICE_ID ?? "agent-service-001",
17
+ destinationTag: Number(process.env.CPX_DESTINATION_TAG ?? "12345"),
18
+ facilitatorUrl: facilitator,
19
+ journalDir: process.env.CPX_EARN_JOURNAL ?? "./private-earn-journal",
20
+ method: "POST",
21
+ path: "/paid",
22
+ onPaid: async (_request, _settlement) =>
23
+ Response.json({ delivered: true, result: "Replace with your service" })
24
+ });
25
+
26
+ await earn.initialize();
27
+
28
+ createServer(async (req, res) => {
29
+ try {
30
+ const headers = new Headers();
31
+ for (const [key, value] of Object.entries(req.headers)) {
32
+ if (value !== undefined)
33
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
34
+ }
35
+
36
+ const request = new Request(
37
+ new URL(req.url, "http://localhost"),
38
+ {
39
+ method: req.method,
40
+ headers,
41
+ ...(["GET", "HEAD"].includes(req.method) ? {} : {
42
+ body: Readable.toWeb(req),
43
+ duplex: "half"
44
+ })
45
+ }
46
+ );
47
+
48
+ const response = await earn.handle(request);
49
+ res.writeHead(response.status, Object.fromEntries(response.headers));
50
+ res.end(Buffer.from(await response.arrayBuffer()));
51
+ } catch (error) {
52
+ console.error("Earn request error:", error?.message);
53
+ res.writeHead(500, { "content-type": "application/json" });
54
+ res.end(JSON.stringify({ error: "Service unavailable" }));
55
+ }
56
+ }).listen(8788, "127.0.0.1", () => {
57
+ console.log("Earn example: http://127.0.0.1:8788/paid");
58
+ console.log("Deploy behind appropriately secured HTTPS for public access.");
59
+ });
60
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cloudpayx/x402-pay",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
- "description": "XRPL-native x402 payment SDK for autonomous AI agents. Secure, wallet-agnostic XRP, RLUSD and Circle USDC payments with spending policies and verified settlement.",
5
+ "description": "XRPL-native x402 Pay + Earn SDK for autonomous AI agents. Purchase and monetize services using XRP, RLUSD, and Circle USDC with guarded payments and verified XRPL settlement.",
6
6
  "dependencies": {
7
7
  "@x402/core": "2.28.0",
8
8
  "@x402/fetch": "2.28.0",
@@ -39,6 +39,15 @@
39
39
  "scripts/trusted-xrpl-recovery.mjs",
40
40
  "types/",
41
41
  "README.md",
42
- "examples/quickstart-validation.mjs"
42
+ "examples/quickstart-validation.mjs",
43
+ "scripts/seller-earn.mjs",
44
+ "scripts/seller-node.mjs",
45
+ "scripts/seller-http.mjs",
46
+ "scripts/seller-proof-guard.mjs",
47
+ "scripts/seller-transaction-guard.mjs",
48
+ "scripts/seller-ledger-verifier.mjs",
49
+ "AGENTS.md",
50
+ "docs/",
51
+ "examples/earn-http-server.mjs"
43
52
  ]
44
53
  }
@@ -74,7 +74,7 @@ export function createJournaledSigner({signer, journal, intentId, hashSignedTx,
74
74
  * createPaymentClient MUST return a guarded SDK instance whose oneShot is ON.
75
75
  * Returning HTTP 200 is intentionally not treated as validated settlement.
76
76
  */
77
- export async function executeDurablePurchase({journal, intent, signer, hashSignedTx, maxFeeDrops, createPaymentClient, request, reserveBudget}) {
77
+ export async function executeDurablePurchase({journal, intent, signer, hashSignedTx, maxFeeDrops, createPaymentClient, request, reserveBudget, releaseUnsigned}) {
78
78
  requireCondition(typeof journal?.reserve === 'function', 'CPX_JOURNAL_REQUIRED');
79
79
  requireCondition(typeof createPaymentClient === 'function', 'CPX_CLIENT_FACTORY_REQUIRED');
80
80
  requireCondition(typeof reserveBudget === 'function', 'CPX_BUDGET_RESERVATION_REQUIRED');
@@ -83,8 +83,25 @@ export async function executeDurablePurchase({journal, intent, signer, hashSigne
83
83
  // Must hold durable capacity before constructing any payment client, HTTP or signer.
84
84
  // A crash between intent and budget reservation intentionally blocks retry.
85
85
  await reserveBudget(intent);
86
- const journaledSigner = createJournaledSigner({signer, journal, intentId: intent.intentId, hashSignedTx, maxFeeDrops});
86
+ let signingAttempted = false;
87
+
88
+ const watchedSigner = {
89
+ classicAddress: signer.classicAddress,
90
+ async sign(tx) {
91
+ signingAttempted = true;
92
+ return signer.sign(tx);
93
+ }
94
+ };
95
+
96
+ const journaledSigner = createJournaledSigner({
97
+ signer: watchedSigner,
98
+ journal,
99
+ intentId: intent.intentId,
100
+ hashSignedTx,
101
+ maxFeeDrops
102
+ });
87
103
  // Caller must use createCpXX402AgentPay({signer:journaledSigner, policy, oneShot:{...}})
104
+ try {
88
105
  const buyer = await createPaymentClient(journaledSigner);
89
106
  requireCondition(typeof buyer?.paidFetch === 'function' && typeof buyer?.oneShotStatus === 'function', 'CPX_ONE_SHOT_CLIENT_REQUIRED');
90
107
  // Never retry automatically. If this throws or times out, the intent stays blocked.
@@ -99,6 +116,21 @@ export async function executeDurablePurchase({journal, intent, signer, hashSigne
99
116
  ...(rejectionBody === undefined ? {} : {rejectionBody}), httpStatus: response.status,
100
117
  paymentResponseHeaderPresent: response.headers?.has?.('payment-response') === true,
101
118
  journalStatus: (await journal.load(intent.intentId)).status};
119
+
120
+ } catch (error) {
121
+ if (!signingAttempted && typeof releaseUnsigned === 'function') {
122
+ try {
123
+ await releaseUnsigned({intent, journal});
124
+ } catch (releaseError) {
125
+ throw new AggregateError(
126
+ [error, releaseError],
127
+ 'CPX_UNSIGNED_RELEASE_NOT_CONFIRMED'
128
+ );
129
+ }
130
+ }
131
+
132
+ throw error;
133
+ }
102
134
  }
103
135
 
104
136
  /** Strict read-only reconciliation. No submission or retry is possible here. */
@@ -129,9 +129,109 @@ export function createPersistentSpendingBudget(journalDir) {
129
129
  await syncDirectory(dir);
130
130
  }
131
131
  }
132
+
133
+ // Only the durable lifecycle may authorize this operation.
134
+ // The original intent remains recorded forever.
135
+ async function releaseUnsigned({intent, journal}) {
136
+ check(record(intent) &&
137
+ typeof intent.intentId === 'string' &&
138
+ typeof intent.payer === 'string' &&
139
+ typeof journal?.load === 'function',
140
+ 'CPX_RELEASE_INVALID_REQUEST');
141
+
142
+ await ensurePrivateDir();
143
+
144
+ const file = fileFor(intent.payer);
145
+ const lock = file + '.lock';
146
+
147
+ let lockFd;
148
+ try {
149
+ lockFd = await open(lock, 'wx', 0o600);
150
+ } catch (err) {
151
+ if (err?.code === 'EEXIST')
152
+ throw new Error('CPX_BUDGET_LOCKED_FAIL_CLOSED');
153
+ throw err;
154
+ }
155
+
156
+ try {
157
+ await lockFd.writeFile(String(process.pid));
158
+ await lockFd.sync();
159
+
160
+ const state = JSON.parse(await readFile(file, 'utf8'));
161
+
162
+ check(state?.version === 1 &&
163
+ state.payer === intent.payer &&
164
+ record(state.caps) &&
165
+ record(state.reserved) &&
166
+ record(state.intentIds),
167
+ 'CPX_BUDGET_CORRUPT');
168
+
169
+ const idHash = createHash('sha256')
170
+ .update(intent.intentId).digest('hex');
171
+
172
+ const entry = state.intentIds[idHash];
173
+
174
+ check(record(entry) &&
175
+ entry.releasedUnsigned !== true &&
176
+ CAP_KEYS.includes(entry.asset) &&
177
+ DROPS.test(entry.units) &&
178
+ DROPS.test(entry.feeDrops),
179
+ 'CPX_RELEASE_INTENT_NOT_RESERVED');
180
+
181
+ const previous = await journal.load(intent.intentId);
182
+
183
+ check(previous.status === 'RESERVED_OUTCOME_UNKNOWN' &&
184
+ previous.payer === intent.payer &&
185
+ previous.seller === intent.seller &&
186
+ previous.asset === intent.asset &&
187
+ previous.amount === intent.amount &&
188
+ previous.resource === intent.resource &&
189
+ !previous.transactionHash &&
190
+ !previous.expected,
191
+ 'CPX_RELEASE_NOT_PROVEN_UNSIGNED');
192
+
193
+ const units = BigInt(entry.units);
194
+ const fee = BigInt(entry.feeDrops);
195
+
196
+ const xrpExposure =
197
+ fee + (entry.asset === 'xrpDrops' ? units : 0n);
198
+
199
+ check(BigInt(state.reserved.xrpDrops) >= xrpExposure &&
200
+ (entry.asset === 'xrpDrops' ||
201
+ BigInt(state.reserved[entry.asset]) >= units),
202
+ 'CPX_RELEASE_BUDGET_UNDERFLOW');
203
+
204
+ if (entry.asset !== 'xrpDrops') {
205
+ state.reserved[entry.asset] = (
206
+ BigInt(state.reserved[entry.asset]) - units
207
+ ).toString();
208
+ }
209
+
210
+ state.reserved.xrpDrops = (
211
+ BigInt(state.reserved.xrpDrops) - xrpExposure
212
+ ).toString();
213
+
214
+ state.intentIds[idHash] = {
215
+ ...entry,
216
+ releasedUnsigned: true
217
+ };
218
+
219
+ await writeAtomic(dir, file, state);
220
+
221
+ return {
222
+ status: 'UNSIGNED_CAPACITY_RELEASED',
223
+ intentId: intent.intentId
224
+ };
225
+ } finally {
226
+ await lockFd.close();
227
+ await unlink(lock);
228
+ await syncDirectory(dir);
229
+ }
230
+ }
231
+
132
232
  async function inspect(payer) {
133
233
  await ensurePrivateDir();
134
234
  return JSON.parse(await readFile(fileFor(payer), 'utf8'));
135
235
  }
136
- return {reserve, inspect};
236
+ return {reserve, inspect, releaseUnsigned};
137
237
  }
@@ -8,3 +8,5 @@
8
8
  export { executeSecureNodePurchase, validateSecureNodePurchase } from './secure-node-purchase.mjs';
9
9
 
10
10
  export { reconcileSecureNodePurchase } from './trusted-xrpl-recovery.mjs';
11
+
12
+ export { createCpXAgentEarn } from "./seller-earn.mjs";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Private pre-release, Node-only cpX x402 Pay entry point.
2
+ * cpX x402 Pay Node.js payment entry point.
3
3
  * One caller-facing function wires safe HTTPS-only Node transport, one-shot
4
4
  * XRPL signing constraints, and the durable same-intent purchase journal.
5
5
  * This does NOT make the lower-level SDK impossible to invoke unsafely;
@@ -79,6 +79,8 @@ export async function executeSecureNodePurchase(options) {
79
79
  intent: options.intent,
80
80
  reserveBudget: intent => budget.reserve({intent,issuer:options.oneShot.issuer,caps:options.budgetCaps,
81
81
  maxFeeDrops:options.maxFeeDrops,maxSendMax:options.oneShot.maxSendMax}),
82
+ releaseUnsigned: ({intent, journal}) =>
83
+ budget.releaseUnsigned({intent, journal}),
82
84
  signer: options.signer,
83
85
  hashSignedTx: trusted.hashSignedTx,
84
86
  maxFeeDrops: options.maxFeeDrops,
@@ -0,0 +1,67 @@
1
+ import { join, resolve } from "node:path";
2
+ import { createCpXSellerConfig, createCpXSellerServer } from "./seller-node.mjs";
3
+ import { createCpXSellerHandler } from "./seller-http.mjs";
4
+ import { createSellerProofGuard } from "./seller-proof-guard.mjs";
5
+ import { createSellerTransactionGuard } from "./seller-transaction-guard.mjs";
6
+ import { confirmSellerLedgerPayment } from "./seller-ledger-verifier.mjs";
7
+
8
+ /**
9
+ * cpX Agent Earn: XRPL-mainnet seller.
10
+ * No wallet secret or signing key required.
11
+ */
12
+ export function createCpXAgentEarn({
13
+ walletAddress,
14
+ asset,
15
+ amount,
16
+ invoiceId,
17
+ destinationTag,
18
+ facilitatorUrl,
19
+ journalDir,
20
+ onPaid,
21
+ method = "POST",
22
+ path = "/paid"
23
+ }) {
24
+ if (typeof journalDir !== "string" || !journalDir.trim()) {
25
+ throw Error("CPX_SELLER_JOURNAL_REQUIRED");
26
+ }
27
+
28
+ if (typeof onPaid !== "function") {
29
+ throw Error("CPX_SELLER_PAID_HANDLER_REQUIRED");
30
+ }
31
+
32
+ const routes = createCpXSellerConfig({
33
+ walletAddress,
34
+ asset,
35
+ amount,
36
+ invoiceId,
37
+ destinationTag,
38
+ method,
39
+ path
40
+ });
41
+
42
+ const server = createCpXSellerServer({
43
+ facilitatorUrl,
44
+ routes
45
+ });
46
+
47
+ const directory = resolve(journalDir);
48
+
49
+ const handle = createCpXSellerHandler({
50
+ httpServer: server,
51
+ proofGuard: createSellerProofGuard({
52
+ journalDir: join(directory, "proofs")
53
+ }),
54
+ transactionGuard: createSellerTransactionGuard({
55
+ journalDir: join(directory, "transactions")
56
+ }),
57
+ confirmLedger: confirmSellerLedgerPayment,
58
+ method,
59
+ path,
60
+ onPaid
61
+ });
62
+
63
+ return Object.freeze({
64
+ initialize: () => server.initialize(),
65
+ handle
66
+ });
67
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * cpX Agent Earn: settlement-gated Fetch API adapter.
3
+ *
4
+ * XRPL seller HTTP adapter with guarded payment verification, replay protection, and settlement-gated delivery.
5
+ * Never calls the protected service before settlement succeeds.
6
+ */
7
+
8
+ function reject(status, error) {
9
+ return new Response(JSON.stringify({ error }), {
10
+ status,
11
+ headers: {
12
+ "Content-Type": "application/json",
13
+ "Cache-Control": "no-store"
14
+ }
15
+ });
16
+ }
17
+
18
+ export function createCpXSellerHandler({
19
+ httpServer,
20
+ proofGuard,
21
+ transactionGuard,
22
+ confirmLedger,
23
+ method = "POST",
24
+ path = "/paid",
25
+ onPaid
26
+ }) {
27
+ if (
28
+ !httpServer ||
29
+ typeof httpServer.processHTTPRequest !== "function" ||
30
+ typeof httpServer.processSettlement !== "function"
31
+ ) {
32
+ throw new Error("CPX_SELLER_HTTP_SERVER_REQUIRED");
33
+ }
34
+
35
+ if (!proofGuard || typeof proofGuard.claim !== "function") {
36
+ throw new Error("CPX_SELLER_PROOF_GUARD_REQUIRED");
37
+ }
38
+
39
+ if (
40
+ !transactionGuard ||
41
+ typeof transactionGuard.claimSignedTransaction !== "function"
42
+ ) {
43
+ throw new Error("CPX_SELLER_TRANSACTION_GUARD_REQUIRED");
44
+ }
45
+
46
+ if (typeof confirmLedger !== "function") {
47
+ throw new Error("CPX_SELLER_LEDGER_VERIFIER_REQUIRED");
48
+ }
49
+
50
+ if (typeof onPaid !== "function") {
51
+ throw new Error("CPX_SELLER_PAID_HANDLER_REQUIRED");
52
+ }
53
+
54
+ if (
55
+ typeof path !== "string" ||
56
+ !path.startsWith("/") ||
57
+ !["GET", "POST"].includes(method)
58
+ ) {
59
+ throw new Error("CPX_SELLER_INVALID_PROTECTED_ROUTE");
60
+ }
61
+
62
+ return async function handle(request) {
63
+ if (!(request instanceof Request)) {
64
+ return reject(400, "INVALID_REQUEST");
65
+ }
66
+
67
+ const requestUrl = new URL(request.url);
68
+
69
+ if (
70
+ request.method !== method ||
71
+ requestUrl.pathname !== path
72
+ ) {
73
+ return reject(404, "ROUTE_NOT_FOUND");
74
+ }
75
+
76
+ const headers = request.headers;
77
+
78
+ const adapter = {
79
+ getHeader: name => headers.get(name) ?? undefined,
80
+ getMethod: () => request.method,
81
+ getPath: () => requestUrl.pathname,
82
+ getUrl: () => request.url,
83
+ getAcceptHeader: () => headers.get("accept") ?? "",
84
+ getUserAgent: () => headers.get("user-agent") ?? "",
85
+ getQueryParams: () =>
86
+ Object.fromEntries(requestUrl.searchParams),
87
+ getQueryParam: name =>
88
+ requestUrl.searchParams.get(name) ?? undefined
89
+ };
90
+
91
+ const context = {
92
+ adapter,
93
+ path: requestUrl.pathname,
94
+ method: request.method,
95
+ paymentHeader:
96
+ headers.get("payment-signature") ??
97
+ headers.get("x-payment") ??
98
+ undefined
99
+ };
100
+
101
+ let result;
102
+
103
+ try {
104
+ result = await httpServer.processHTTPRequest(context);
105
+ } catch {
106
+ return reject(503, "PAYMENT_VERIFICATION_UNAVAILABLE");
107
+ }
108
+
109
+ if (result?.type === "payment-error") {
110
+ const response = result.response;
111
+ const body = response?.body;
112
+ let parsed = body;
113
+ if (typeof body === "string") {
114
+ try { parsed = JSON.parse(body); } catch { parsed = {}; }
115
+ }
116
+ const reason = parsed?.invalidReason ??
117
+ parsed?.invalidMessage ??
118
+ parsed?.errorMessage ??
119
+ parsed?.errorReason ??
120
+ parsed?.error ??
121
+ parsed?.reason ??
122
+ parsed?.verification?.invalidReason ??
123
+ "UNSPECIFIED";
124
+
125
+ if (context.paymentHeader) {
126
+ console.error("[CPX_EARN_VERIFY_REJECTED]", {
127
+ status: response?.status,
128
+ reason: typeof reason === "string"
129
+ ? reason.slice(0, 160) : "UNSPECIFIED",
130
+ responseFields: parsed && typeof parsed === "object"
131
+ ? Object.keys(parsed).slice(0, 20)
132
+ : []
133
+ });
134
+ }
135
+
136
+ if (!response || !Number.isInteger(response.status)) {
137
+ return reject(503, "PAYMENT_RESPONSE_INVALID");
138
+ }
139
+
140
+ return new Response(
141
+ response.body == null
142
+ ? null
143
+ : typeof response.body === "string"
144
+ ? response.body
145
+ : JSON.stringify(response.body),
146
+ {
147
+ status: response.status,
148
+ headers: {
149
+ ...response.headers,
150
+ "Cache-Control": "no-store"
151
+ }
152
+ }
153
+ );
154
+ }
155
+
156
+ // This adapter protects ONLY paid endpoints.
157
+ // An unexpected free-access result must not unlock content.
158
+ if (result?.type !== "payment-verified") {
159
+ return reject(403, "PAYMENT_REQUIRED");
160
+ }
161
+
162
+ // Enforce unsponsored XRPL payments before settlement.
163
+ // Never settle a payment whose verified requirements
164
+ // do not explicitly disable facilitator fee sponsorship.
165
+ if (result.paymentRequirements?.extra?.areFeesSponsored !== false) {
166
+ return reject(403, "CPX_FEE_SPONSORSHIP_NOT_ALLOWED");
167
+ }
168
+
169
+ // The installed XRPL server supports multiple payment flows.
170
+ // Until each flow has been tested, refuse requests that report
171
+ // a pre-handler settlement rather than assuming its outcome.
172
+ if (result.beforeHandlerSettlement !== undefined) {
173
+ return reject(503, "PRESETTLED_FLOW_NOT_YET_SUPPORTED");
174
+ }
175
+
176
+ // A verified proof must be claimed before settlement.
177
+ // Atomic persistent claims block concurrent and repeated requests.
178
+ const proof = context.paymentHeader;
179
+
180
+ if (typeof proof !== "string" || !proof.trim()) {
181
+ return reject(403, "PAYMENT_PROOF_MISSING");
182
+ }
183
+
184
+ try {
185
+ if (!proofGuard.claim(proof)) {
186
+ return reject(409, "PAYMENT_PROOF_ALREADY_CLAIMED");
187
+ }
188
+ } catch {
189
+ return reject(503, "PAYMENT_PROOF_GUARD_UNAVAILABLE");
190
+ }
191
+
192
+ // Identify the signed XRPL transaction, not just its HTTP encoding.
193
+ const signedTxBlob = result.paymentPayload?.payload?.signedTxBlob;
194
+
195
+ if (typeof signedTxBlob !== "string" || !signedTxBlob) {
196
+ return reject(403, "SIGNED_XRPL_TRANSACTION_MISSING");
197
+ }
198
+
199
+ let transactionClaim;
200
+ try {
201
+ transactionClaim =
202
+ transactionGuard.claimSignedTransaction(signedTxBlob);
203
+ } catch {
204
+ return reject(503, "TRANSACTION_GUARD_UNAVAILABLE");
205
+ }
206
+
207
+ if (!transactionClaim?.claimed) {
208
+ return reject(409, "XRPL_TRANSACTION_ALREADY_CLAIMED");
209
+ }
210
+
211
+ let settlement;
212
+
213
+ try {
214
+ settlement = await httpServer.processSettlement(
215
+ result.paymentPayload,
216
+ result.paymentRequirements,
217
+ result.declaredExtensions,
218
+ { request: context }
219
+ );
220
+ } catch {
221
+ // A settlement timeout may be indeterminate.
222
+ // Never retry settlement automatically here.
223
+ return reject(503, "SETTLEMENT_OUTCOME_UNCERTAIN");
224
+ }
225
+
226
+ if (settlement?.success !== true) {
227
+ const reason = settlement?.errorReason ??
228
+ settlement?.error ?? settlement?.reason ?? "UNSPECIFIED";
229
+ console.error("[CPX_EARN_SETTLEMENT_REJECTED]", {
230
+ reason: typeof reason === "string"
231
+ ? reason.slice(0, 160) : "UNSPECIFIED"
232
+ });
233
+ return reject(402, "PAYMENT_NOT_SETTLED");
234
+ }
235
+
236
+ // A successful facilitator response must identify the exact
237
+ // signed XRPL transaction claimed before settlement.
238
+ if (
239
+ typeof transactionClaim?.hash !== "string" ||
240
+ !/^[0-9A-Fa-f]{64}$/.test(transactionClaim.hash) ||
241
+ typeof settlement.transaction !== "string" ||
242
+ settlement.transaction.toUpperCase() !==
243
+ transactionClaim.hash.toUpperCase() ||
244
+ settlement.network !== "xrpl:0"
245
+ ) {
246
+ return reject(503, "SETTLEMENT_IDENTITY_MISMATCH");
247
+ }
248
+
249
+ // Independently confirm validated XRPL settlement.
250
+ // A facilitator's success response alone cannot unlock content.
251
+ let ledgerReceipt;
252
+ try {
253
+ ledgerReceipt = await confirmLedger({
254
+ signedTxBlob,
255
+ requirements: result.paymentRequirements
256
+ });
257
+ } catch {
258
+ return reject(503, "LEDGER_CONFIRMATION_UNAVAILABLE");
259
+ }
260
+
261
+ if (
262
+ ledgerReceipt?.validated !== true ||
263
+ ledgerReceipt?.network !== "xrpl:0" ||
264
+ typeof ledgerReceipt.transaction !== "string" ||
265
+ ledgerReceipt.transaction.toUpperCase() !==
266
+ transactionClaim.hash.toUpperCase()
267
+ ) {
268
+ return reject(503, "LEDGER_CONFIRMATION_MISMATCH");
269
+ }
270
+
271
+ // Only successful, independently confirmed settlement
272
+ // reaches the protected service.
273
+ try {
274
+ const paidResponse = await onPaid(request, settlement);
275
+
276
+ if (!(paidResponse instanceof Response)) {
277
+ return reject(500, "PAID_HANDLER_INVALID_RESPONSE");
278
+ }
279
+
280
+ paidResponse.headers.set("Cache-Control", "no-store");
281
+
282
+ return paidResponse;
283
+ } catch {
284
+ return reject(500, "PAID_SERVICE_FAILED");
285
+ }
286
+ };
287
+ }