Getting started with x402Fetch
Make x402 payments with a TypeScript/JavaScript client on XRPL.
Step 1: Install dependencies
npm install x402-xrpl xrpl dotenvStep 2: Set your environment variables
Create a .env file with your configuration:
RESOURCE_URL=http://localhost:8080/hello XRPL_NETWORK=xrpl:1 XRPL_BUYER_SEED=sEd... # your wallet seed XRPL_TESTNET_WS_URL=wss://s.altnet.rippletest.net:51233 XRPL_SKIP_FAUCET=false XRPL_TESTNET_FAUCET_URL=https://faucet.altnet.rippletest.net/accounts
Step 3: Create a paid fetch client
Create a client.js file (if you're using ESM imports, set "type": "module" in your package.json or use .mjs).
import dotenv from "dotenv";
import { Wallet } from "xrpl";
import {
decodePaymentRequiredHeader,
decodePaymentResponseHeader,
x402Fetch,
} from "x402-xrpl";
dotenv.config({ override: false });
const resourceUrl = process.env.RESOURCE_URL ?? "http://127.0.0.1:8080/hello";
const network = process.env.XRPL_NETWORK ?? "xrpl:1";
const seed = process.env.XRPL_BUYER_SEED;
const faucetUrl =
process.env.XRPL_TESTNET_FAUCET_URL ?? "https://faucet.altnet.rippletest.net/accounts";
const skipFaucet = ["1", "true", "yes"].includes(
String(process.env.XRPL_SKIP_FAUCET ?? "false").toLowerCase(),
);
function wsUrlForNetwork(net) {
if (net === "xrpl:0") {
return process.env.XRPL_MAINNET_WS_URL ?? "wss://s1.ripple.com:51233";
}
if (net === "xrpl:1") {
return process.env.XRPL_TESTNET_WS_URL ?? "wss://s.altnet.rippletest.net:51233";
}
if (net === "xrpl:2") {
return process.env.XRPL_DEVNET_WS_URL ?? "wss://s.devnet.rippletest.net:51233";
}
throw new Error(`unsupported_network:${net}`);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fundWalletIfNeeded(address) {
if (skipFaucet) return;
if (network !== "xrpl:1") return;
// eslint-disable-next-line no-console
console.log(`Funding buyer wallet via faucet: ${address}`);
const resp = await fetch(faucetUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ destination: address }),
});
// eslint-disable-next-line no-console
console.log(`Faucet response: ${resp.status}`);
// Ledger confirmation is asynchronous; give it a moment.
await sleep(10_000);
}
async function main() {
if (!seed) {
throw new Error("XRPL_BUYER_SEED is required");
}
const buyer = Wallet.fromSeed(seed);
// eslint-disable-next-line no-console
console.log(`Buyer: ${buyer.classicAddress}`);
// eslint-disable-next-line no-console
console.log(`Resource URL: ${resourceUrl}`);
await fundWalletIfNeeded(buyer.classicAddress);
const fetchPaid = x402Fetch({
wallet: buyer,
networkFilter: network,
schemeFilter: "exact",
wsUrlForNetwork,
});
const resp = await fetchPaid(resourceUrl, {
method: "GET",
headers: { accept: "application/json" },
});
// eslint-disable-next-line no-console
console.log(`\nHTTP ${resp.status}`);
// eslint-disable-next-line no-console
console.log(await resp.text());
const paymentResponse = resp.headers.get("PAYMENT-RESPONSE");
if (paymentResponse) {
const decoded = decodePaymentResponseHeader(paymentResponse);
// eslint-disable-next-line no-console
console.log("\nDecoded PAYMENT-RESPONSE:");
// eslint-disable-next-line no-console
console.log(JSON.stringify(decoded, null, 2));
return;
}
const paymentRequired = resp.headers.get("PAYMENT-REQUIRED");
if (paymentRequired) {
const decoded = decodePaymentRequiredHeader(paymentRequired);
// eslint-disable-next-line no-console
console.log("\nDecoded PAYMENT-REQUIRED:");
// eslint-disable-next-line no-console
console.log(JSON.stringify(decoded, null, 2));
}
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
process.exitCode = 1;
});Step 4: Run the client
node client.jsYour client will automatically handle the HTTP 402 flow by building a presigned XRPL Payment tx blob and retrying the request with a PAYMENT-SIGNATURE header.