#!/usr/bin/env node
// Pay PayGuard's Solana devnet x402 route with a devnet keypair and print the result + settlement tx.
// SOLANA_KEYPAIR=/path/to/devnet-keypair.json node scripts/solana-devnet-pay.mjs [url]
// Env: SOLANA_KEYPAIR (Solana CLI JSON, 64 bytes; required), SOLANA_DEVNET_RPC (default https://api.devnet.solana.com).
// The payer needs devnet USDC (https://faucet.circle.com -> Solana Devnet) and no SOL (the facilitator pays the fee).
// --local-rpc: for hosts that can't reach a devnet RPC. Serves the two read calls the x402 client makes from a local
// stub: the USDC mint's (static) decimals/owner, and a fresh devnet blockhash taken from the PayAI facilitator's public
// /supported. The signed transaction is real; the x402.org facilitator simulates and submits it on devnet.
import { readFileSync } from 'node:fs';
import { createServer } from 'node:http';
import { createKeyPairSignerFromBytes } from '@solana/kit';
import { x402Client, wrapFetchWithPayment, decodePaymentResponseHeader } from '@x402/fetch';
import { ExactSvmScheme } from '@x402/svm/exact/client';
const args = process.argv.slice(2);
const localRpc = args.includes('--local-rpc');
const url = args.find((a) => !a.startsWith('--')) || 'https://api.youraitools.co/payguard/solana-devnet/v1/address?address=0x098B716B8Aaf21512996dC57EB0615e2383E2f96';
const DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
const USDC = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
const bytes = new Uint8Array(JSON.parse(readFileSync(process.env.SOLANA_KEYPAIR, 'utf8')));
const signer = await createKeyPairSignerFromBytes(bytes);
console.log('payer', signer.address);
let rpcUrl = process.env.SOLANA_DEVNET_RPC;
let srv;
if (localRpc) {
const sup = await (await fetch('https://facilitator.payai.network/supported')).json();
const k = sup.kinds.find((x) => x.network === DEVNET && x.extra?.recentBlockhash);
if (!k) throw new Error('no devnet blockhash from PayAI /supported');
// SPL Mint layout (82 bytes): mint_authority COption<Pubkey>, supply u64, decimals u8, is_initialized bool, freeze_authority COption<Pubkey>
const mint = new Uint8Array(82); mint[44] = 6; mint[45] = 1;
const b64 = Buffer.from(mint).toString('base64');
srv = createServer((req, res) => {
let body = ''; req.on('data', (d) => (body += d)); req.on('end', () => {
const m = JSON.parse(body); let result;
if (m.method === 'getAccountInfo') result = { context: { slot: 1 }, value: { data: [b64, 'base64'], executable: false, lamports: 1461600, owner: TOKEN_PROGRAM, rentEpoch: 0, space: 82 } };
else if (m.method === 'getLatestBlockhash') result = { context: { slot: 1 }, value: { blockhash: k.extra.recentBlockhash, lastValidBlockHeight: Number(k.extra.lastValidBlockHeight) } };
else { res.end(JSON.stringify({ jsonrpc: '2.0', id: m.id, error: { code: -32601, message: `stub: ${m.method}` } })); return; }
res.setHeader('content-type', 'application/json'); res.end(JSON.stringify({ jsonrpc: '2.0', id: m.id, result }));
});
}).listen(0);
rpcUrl = `http://127.0.0.1:${srv.address().port}`;
}
// Sanity: the route's fee payer must match the facilitator's current Solana signer.
const sup = await (await fetch('https://x402.org/facilitator/supported')).json();
const fp = sup.kinds.find((x) => x.network === DEVNET && x.scheme === 'exact')?.extra?.feePayer;
console.log('x402.org devnet feePayer', fp);
const client = new x402Client().register(DEVNET, new ExactSvmScheme(signer, rpcUrl ? { rpcUrl } : undefined));
const pay = wrapFetchWithPayment(fetch, client);
const t0 = Date.now();
const res = await pay(url, { headers: { accept: 'application/json' } });
const text = await res.text();
console.log('status', res.status, `${Date.now() - t0} ms`);
const pr = res.headers.get('payment-response');
if (pr) {
const s = decodePaymentResponseHeader(pr);
console.log('settlement', JSON.stringify(s));
if (s.transaction) console.log('explorer', `https://explorer.solana.com/tx/${s.transaction}?cluster=devnet`);
}
const req = res.headers.get('payment-required');
if (res.status === 402 && req) { try { console.log('402 reason', JSON.parse(Buffer.from(req, 'base64').toString()).error); } catch {} }
console.log(text.slice(0, 1500));
srv?.close();
process.exit(res.ok ? 0 : 1);
agentgate-source: code mirror on Cloudflare Pages. All repos