# 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.\
\ <br>

**SVM setup:**<br>

```
// --------------------------------------------------------
//  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.**<br>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hashhunter.ai/hash-hunter-x402-endpoint/guide-to-test-x402-server.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
