5️⃣Guide to test x402 server

Prerequesites:

Installed Typescript Installed ts-node Installed dotenv Installed node-fetch Installed tweetnacl Installed bs58 env file with public and private key for Solana wallet. and API URL - htttp://api.hashhunter.app/api/process Store the private key in json format.

SVM setup:

// --------------------------------------------------------
//  LOAD ENVIRONMENT
// --------------------------------------------------------
import dotenv from "dotenv";
dotenv.config();

import fetch from "node-fetch";
import nacl from "tweetnacl";
import bs58 from "bs58";

import {
  Connection,
  VersionedTransaction,
  Transaction as LegacyTx,
  MessageV0,
  Keypair,
  PublicKey
} from "@solana/web3.js";


// --------------------------------------------------------
//  ENV VARIABLES (YOUR EXACT ENV)
// --------------------------------------------------------
const API_URL = process.env.API_URL;
const PRIVATE_KEY: number[] = JSON.parse(process.env.SOL_PRIVATE_KEY!);
const PUBLIC_KEY_BASE58 = process.env.SOL_PUBLIC_KEY!;

// ✔ Public Solana RPC placeholder (unless overridden)
const RPC_URL =
  process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com";

// Validate env
if (!API_URL) throw new Error("Missing API_URL");
if (!PRIVATE_KEY) throw new Error("Missing SOL_PRIVATE_KEY");
if (!PUBLIC_KEY_BASE58) throw new Error("Missing SOL_PUBLIC_KEY");

// Construct signing keypair
const keypair = Keypair.fromSecretKey(new Uint8Array(PRIVATE_KEY));
const walletPublicKey = new PublicKey(PUBLIC_KEY_BASE58);

console.log("🔑 Using Wallet (ENV):", walletPublicKey.toBase58());


// --------------------------------------------------------
//  HELPERS
// --------------------------------------------------------
function base64ToUint8Array(b64: string) {
  return new Uint8Array(Buffer.from(b64, "base64"));
}

async function post(url: string, body: any, headers: any = {}) {
  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json", ...headers },
    body: JSON.stringify(body),
  });

  return await res.json();
}


// --------------------------------------------------------
//  BASE58 ADDRESS FIXER
// --------------------------------------------------------
const BASE58_REGEX = /\b[1-9A-HJ-NP-Za-km-z]{30,60}\b/g;

// Fix token mint case issues
function normalizeSolanaAddresses(nlp: string): string {
  const matches = nlp.match(BASE58_REGEX);
  if (!matches) return nlp;

  let fixed = nlp;

  for (const token of matches) {
    try {
      const decoded = bs58.decode(token);

      // Valid Solana address = 32 bytes
      if (decoded.length === 32) {
        const re = new RegExp(token, "gi");
        fixed = fixed.replace(re, token);
      }
    } catch {}
  }

  return fixed;
}


// --------------------------------------------------------
//  STEP 1 — NLP → operationId
// --------------------------------------------------------
async function initOperation(nlp: string) {
  const cleanedNlp = normalizeSolanaAddresses(nlp);

  const payload = {
    messages: [{ role: "user", content: cleanedNlp }],
    wallet: walletPublicKey.toBase58(),   // ← EXACT ENV PUBLIC KEY
    chain: "solana",
  };

  const initRes = await post(API_URL!, payload);
  console.log("STEP 1 INIT:", initRes);

  if (initRes.operationId) return initRes.operationId;
  if (initRes.operations?.[0]?.id) return initRes.operations[0].id;

  throw new Error(`API returned no operationId: ${JSON.stringify(initRes)}`);
}


// --------------------------------------------------------
//  STEP 2 — Handle X402 challenge
// --------------------------------------------------------
async function handleX402(operationId: string) {
  const challengeRes = await post(API_URL!, { operationId });
  console.log("STEP 2 CHALLENGE:", challengeRes);

  if (challengeRes.error !== "Token payment required") {
    return challengeRes;
  }

  const cfg = challengeRes.accepts[0];

  const challenge = {
    scheme: cfg.scheme,
    network: cfg.network,
    resource: cfg.resource,
    payTo: cfg.payTo,
    asset: cfg.asset,
    amount: "0",
  };

  const challengeBytes = Buffer.from(JSON.stringify(challenge));
  const signature = nacl.sign.detached(challengeBytes, keypair.secretKey);
  const paymentProof = Buffer.from(signature).toString("base64");

  console.log("X-PAYMENT:", paymentProof);

  const txData = await post(
    API_URL!,
    { operationId },
    { "X-PAYMENT": paymentProof }
  );

  console.log("STEP 3 TX DATA:", txData);
  return txData;
}


// --------------------------------------------------------
//  STEP 3 — Deserialize, sign, broadcast
// --------------------------------------------------------
async function processTx(base64Tx: string) {
  const txBytes = base64ToUint8Array(base64Tx);

  let solTx: VersionedTransaction | LegacyTx;

  try {
    solTx = VersionedTransaction.deserialize(txBytes);
  } catch {
    solTx = LegacyTx.from(txBytes);
  }

  const connection = new Connection(RPC_URL, "confirmed");
  const { blockhash } = await connection.getLatestBlockhash("confirmed");

  // Versioned
  if (solTx instanceof VersionedTransaction) {
    const old = solTx.message;

    solTx = new VersionedTransaction(
      new MessageV0({
        header: old.header,
        staticAccountKeys: old.staticAccountKeys,
        recentBlockhash: blockhash,
        compiledInstructions: old.compiledInstructions,
        addressTableLookups: old.addressTableLookups,
      })
    );

    solTx.sign([keypair]);
  }

  // Legacy
  else {
    solTx.recentBlockhash = blockhash;
    solTx.sign(keypair);
  }

  const sig = await connection.sendRawTransaction(solTx.serialize(), {
    skipPreflight: false,
    preflightCommitment: "confirmed",
  });

  console.log("🚀 TX SENT:", sig);

  await connection.confirmTransaction(sig, "confirmed");

  console.log("✅ TX CONFIRMED:", sig);
  return sig;
}


// --------------------------------------------------------
//  MAIN PIPELINE
// --------------------------------------------------------
async function main() {
  const nlp =
    `buy sh3nQze56RGQFBxr3FZidbbVNVh2oUctgt2sGP4pump with 0.1 sol`;

  const operationId = await initOperation(nlp);
  const txData = await handleX402(operationId);

  if (!txData.transactions?.[0]?.data) {
    throw new Error("No transaction returned by API");
  }

  await processTx(txData.transactions[0].data);
}

main().catch((err) => {
  console.error("❌ ERROR:");
  console.error(err);
});


In order to bypass the default protocol paywall you will need to sign a transaction that costs 0USDC to push your transaction through the PayAI facilitator.

Last updated