---
name: x402-agent-payments
description: Instructions for AI agents with Solana wallets to pay for TTC Box API access using the x402 protocol. No facilitator needed — agents submit USDC payments directly on-chain.
license: Proprietary
compatibility: Designed for Claude Code (or similar products)
metadata:
  author: ttcbox
  version: "2.0"
  category: "payments"
  emoji: "💸"
---

# x402 Payment Instructions for AI Agents

You are an AI agent with a Solana wallet. You can access any TTC Box `/api/v1/*` endpoint by paying 0.05 USDC per request directly on Solana mainnet. No API key, no facilitator, no intermediary.

## Constants

| Field           | Value                                          |
| --------------- | ---------------------------------------------- |
| Base URL        | `https://ttc.box`                              |
| Treasury Wallet | `2v4XjdTjHK7qKEc8BkCeCWFrZmGSJv32ZGyv27zw3jc5` |
| USDC Mint       | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
| Price           | 0.05 USDC (50,000 microUSDC) per call          |
| Network         | Solana Mainnet                                 |
| Payment Timeout | 300 seconds                                    |

> **Note:** The treasury wallet address is returned in the 402 challenge response (`paymentRequired.accepts`). Always prefer using the address from the challenge to ensure accuracy.

## Payment Flow

### Step 1: Call the endpoint (no auth)

Make a request to any `/api/v1/*` endpoint without authentication. You will receive a `402 Payment Required` response with payment details in `paymentRequired.accepts`. You can skip this step if you already know the treasury wallet and price.

### Step 2: Send USDC on Solana

Transfer exactly **0.05 USDC** (50,000 microUSDC) to the treasury wallet. Your wallet needs USDC for the payment and a small amount of SOL (~0.000005) for gas. Wait for the transaction to reach `confirmed` status before proceeding.

### Step 3: Replay the request with the payment header

Encode your payment proof as base64 JSON and attach it as the `x-x402-payment` header:

- `signature` — the Solana transaction signature from Step 2
- `payer` — your wallet's public key (base58)
- `amount` — `"50000"` (string)
- `timestamp` — `Math.floor(Date.now() / 1000)` (epoch seconds)
- `payTo` — the treasury wallet address (optional, defaults to treasury)

Format: `"x402:" + base64(JSON.stringify(paymentData))`

### Step 4: Use the response

On successful verification you get a normal `200` response with the requested data.

## Rules

- **One payment = one request.** Each tx signature can only be used once.
- **Pay before you call.** The transaction must be confirmed on-chain before you send the header.
- **Payments expire.** Transactions older than 300 seconds are rejected.
- **USDC only.** No SOL, no other tokens.
- **You pay gas.** Solana fees are ~0.000005 SOL per tx. You need SOL in your wallet for gas.
- **No facilitator needed.** You submit the transaction yourself. The server verifies it on-chain. That's it.

## Available Endpoints

Any `/api/v1/*` route accepts x402 payment. Key endpoints:

| Endpoint                          | Data                     |
| --------------------------------- | ------------------------ |
| `/api/v1/markets/ttc-scanner`     | Asset technical scanner  |
| `/api/v1/markets/news`            | Crypto news feed         |
| `/api/v1/markets/hybrid-tickers`  | Aggregated ticker data   |
| `/api/v1/markets/funding-rates`   | Perpetual funding rates  |
| `/api/v1/markets/open-interest`   | Open interest data       |
| `/api/v1/markets/listings`        | New token listings       |
| `/api/v1/markets/insights`        | Market insights          |
| `/api/v1/markets/calendar`        | Economic calendar        |
| `/api/v1/markets/swap-volume`     | DEX swap volumes         |
| `/api/v1/markets/volume-snapshot` | Volume snapshots         |
| `/api/v1/markets/quakes`          | Earthquake events        |
| `/api/v1/exchanges`               | Unified Trading API      |
| `/api/v1/exchanges/latency`       | Exchange latency monitor |

## Error Handling

| Status              | Meaning                          | Action                                             |
| ------------------- | -------------------------------- | -------------------------------------------------- |
| `402` (no header)   | Payment required                 | Send USDC and retry with header                    |
| `402` (with header) | Invalid/expired/replayed payment | Check tx confirmed, check amount, check age < 300s |
| `200`               | Success                          | Parse response body as JSON                        |

## Solana Rust CLI (Recommended)

A Rust-based CLI with built-in wallet management and USDC transfers. Download from source: `https://gitlab.com/tradingtoolcrypto/solana-rust-cli`

### Install

```bash
git clone https://gitlab.com/tradingtoolcrypto/solana-rust-cli.git
cd solana-rust-cli
cp .env.example .env
```

### 1. Generate a wallet

```bash
cargo run --bin new_wallet
# Pubkey:
# 7xKXtg2CW87aJRHSvqBuGrT8...
#
# Base58 private key:
# 4wBqpZM9k3JRvNeGfXkCzUiE7...
#
# JSON private key:
# [174, 47, 154, 16, 202, ...]
#
# SIGNER_KEYPAIR saved to .env
```

The keypair is saved as a base58 string to `SIGNER_KEYPAIR` in your `.env` file automatically. You still need to set `WALLET_PUBKEY` and optionally `RPC_URL` in `.env`.

### 2. Fund your wallet

Send SOL (for gas) and USDC (for payments) to your wallet's pubkey. Check balances:

```bash
# SOL balance — requires RPC_URL + WALLET_PUBKEY in .env
cargo run --bin sol_balance
# Wallet: 7xKXtg2CW87a...
# Balance: 0.5 SOL (500000000 lamports)

# USDC balance — also needs MINT_ACCOUNT_PUBKEY in .env
# Set MINT_ACCOUNT_PUBKEY=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
cargo run --bin associated_token_balance
# Wallet pubkey: 7xKXtg2CW87a...
# Mint account: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
# Associated token account: F59618aQB8r6...
# Amount: 12.50
# Decimals: 6
```

### 3. Send $0.05 USDC to the treasury

```bash
cargo run --bin send_usdc -- 0.05 2v4XjdTjHK7qKEc8BkCeCWFrZmGSJv32ZGyv27zw3jc5
# USDC sent successfully.
# Signature: 5VqBgkR9xH7mZxTnPa8K3VqBgkR9xH7mZxTnPa8K3...
# Amount: 0.05 USDC
# Recipient: 2v4XjdTjHK7qKEc8BkCeCWFrZmGSJv32ZGyv27zw3jc5
# Recipient ATA: F59618aQB8r6asXeMcB9jWuY6NEx1VduT9yFo1GTi1ks
```

`send_usdc` reads `SIGNER_KEYPAIR` from `.env`. `RPC_URL` is optional — defaults to `https://api.mainnet-beta.solana.com` if not set. Auto-creates the recipient's USDC token account if needed.

### 4. Call the API with payment proof

Use the signature printed by `send_usdc`:

```bash
SIGNATURE="5VqBgkR9xH7mZxTnPa8K3VqBgkR9xH7mZxTnPa8K3..."
PAYER="7xKXtg2CW87a..."  # your wallet pubkey

PAYLOAD=$(echo -n '{"signature":"'$SIGNATURE'","payer":"'$PAYER'","amount":"50000","timestamp":'$(date +%s000)'}' | base64)

curl -s "https://ttc.box/api/v1/markets/ttc-scanner?symbol=BTCUSDT&timeframe=1h" \
  -H "x-x402-payment: x402:$PAYLOAD"
```

### All CLI commands

| Command                                    | Purpose                 | Env Vars                                                                        |
| ------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------- |
| `cargo run --bin new_wallet`               | Generate keypair        | None                                                                            |
| `cargo run --bin send_usdc -- <amt> <to>`  | Transfer USDC (mainnet) | `SIGNER_KEYPAIR`, `RPC_URL` (optional)                                          |
| `cargo run --bin send_sol -- <amt> <to>`   | Transfer native SOL     | `SIGNER_KEYPAIR`, `RPC_URL`                                                     |
| `cargo run --bin send_spl`                 | Transfer any SPL token  | `SIGNER_KEYPAIR`, `RPC_URL`, `MINT_ACCOUNT_PUBKEY`, `RECEIVER_PUBKEY`, `AMOUNT` |
| `cargo run --bin sol_balance`              | Check SOL balance       | `RPC_URL`, `WALLET_PUBKEY`                                                      |
| `cargo run --bin associated_token_balance` | Check SPL token balance | `RPC_URL`, `WALLET_PUBKEY`, `MINT_ACCOUNT_PUBKEY`                               |

## JavaScript Examples

### Option A: With `@solana/web3.js` + `@solana/spl-token`

```javascript
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import {
  getAssociatedTokenAddress,
  createTransferInstruction,
} from "@solana/spl-token";

const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const TREASURY = new PublicKey("2v4XjdTjHK7qKEc8BkCeCWFrZmGSJv32ZGyv27zw3jc5");

async function callTTCWithPayment(endpoint) {
  const connection = new Connection("https://api.mainnet-beta.solana.com");

  // 1. Send 0.05 USDC
  const senderATA = await getAssociatedTokenAddress(
    USDC_MINT,
    wallet.publicKey,
  );
  const treasuryATA = await getAssociatedTokenAddress(USDC_MINT, TREASURY);
  const ix = createTransferInstruction(
    senderATA,
    treasuryATA,
    wallet.publicKey,
    50_000,
  );
  const tx = new Transaction().add(ix);
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
  tx.feePayer = wallet.publicKey;
  const signed = await wallet.signTransaction(tx);
  const signature = await connection.sendRawTransaction(signed.serialize());
  await connection.confirmTransaction(signature, "confirmed");

  // 2. Build payment header
  const payment = {
    signature,
    payer: wallet.publicKey.toBase58(),
    amount: "50000",
    timestamp: Math.floor(Date.now() / 1000),
    payTo: TREASURY.toBase58(),
  };
  const header =
    "x402:" + Buffer.from(JSON.stringify(payment)).toString("base64");

  // 3. Call the endpoint
  const res = await fetch(`https://ttc.box${endpoint}`, {
    headers: { "x-x402-payment": header },
  });
  return res.json();
}

const news = await callTTCWithPayment("/api/v1/markets/news");
```

### Option B: Without Solana SDK — raw `fetch` + JSON-RPC

No Solana SDK needed. Uses `tweetnacl` (signing), `bs58` (encoding), `@noble/curves` (PDA derivation), and Node.js `crypto` (sha256). Verified to produce byte-identical transactions to `@solana/web3.js`.

```javascript
import nacl from "tweetnacl";
import bs58 from "bs58";
import crypto from "crypto";
import { ed25519 } from "@noble/curves/ed25519";

// --- Constants ---

const RPC = "https://api.mainnet-beta.solana.com";
const TREASURY = "2v4XjdTjHK7qKEc8BkCeCWFrZmGSJv32ZGyv27zw3jc5";
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
const ATA_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
const AMOUNT = 50_000; // 0.05 USDC in microUSDC (6 decimals)

// --- JSON-RPC Helper ---

async function rpc(method, params = []) {
  const res = await fetch(RPC, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
  });
  const json = await res.json();
  if (json.error) throw new Error(`RPC ${method}: ${json.error.message}`);
  return json.result;
}

// --- Ed25519 / PDA Helpers ---

function isOnCurve(bytes) {
  try {
    ed25519.ExtendedPoint.fromHex(Buffer.from(bytes));
    return true;
  } catch {
    return false;
  }
}

function findPDA(seeds, programId) {
  const programIdBytes =
    typeof programId === "string" ? bs58.decode(programId) : programId;
  for (let bump = 255; bump >= 0; bump--) {
    const hasher = crypto.createHash("sha256");
    for (const seed of seeds) hasher.update(Buffer.from(seed));
    hasher.update(Buffer.from([bump]));
    hasher.update(Buffer.from(programIdBytes));
    hasher.update(Buffer.from("ProgramDerivedAddress"));
    const hash = new Uint8Array(hasher.digest());
    if (!isOnCurve(hash)) return { address: hash, bump };
  }
  throw new Error("Could not find valid PDA bump");
}

function getATA(wallet, mint) {
  const walletBytes = typeof wallet === "string" ? bs58.decode(wallet) : wallet;
  const mintBytes = typeof mint === "string" ? bs58.decode(mint) : mint;
  const tokenProgramBytes = bs58.decode(TOKEN_PROGRAM_ID);
  const { address } = findPDA(
    [walletBytes, tokenProgramBytes, mintBytes],
    ATA_PROGRAM_ID,
  );
  return bs58.encode(address);
}

// --- Transaction Serialization ---

function compactU16(value) {
  if (value < 0x80) return Buffer.from([value]);
  if (value < 0x4000) return Buffer.from([(value & 0x7f) | 0x80, value >> 7]);
  return Buffer.from([
    (value & 0x7f) | 0x80,
    ((value >> 7) & 0x7f) | 0x80,
    value >> 14,
  ]);
}

function buildTransactionMessage(
  walletPubkey,
  senderATA,
  treasuryATA,
  blockhash,
) {
  const senderATABytes =
    typeof senderATA === "string" ? bs58.decode(senderATA) : senderATA;
  const treasuryATABytes =
    typeof treasuryATA === "string" ? bs58.decode(treasuryATA) : treasuryATA;
  const tokenProgramBytes = bs58.decode(TOKEN_PROGRAM_ID);
  const blockhashBytes =
    typeof blockhash === "string" ? bs58.decode(blockhash) : blockhash;

  const header = Buffer.from([1, 0, 1]); // 1 signer, 0 ro-signed, 1 ro-unsigned
  const accountKeys = Buffer.concat([
    Buffer.from(walletPubkey), // [0] signer + fee payer
    Buffer.from(senderATABytes), // [1] source ATA (writable)
    Buffer.from(treasuryATABytes), // [2] dest ATA (writable)
    Buffer.from(tokenProgramBytes), // [3] token program (readonly)
  ]);

  const instructionData = Buffer.alloc(9);
  instructionData.writeUInt8(3, 0); // SPL Token Transfer
  instructionData.writeBigUInt64LE(BigInt(AMOUNT), 1);

  return Buffer.concat([
    header,
    compactU16(4), // 4 account keys
    accountKeys, // 4 * 32 = 128 bytes
    Buffer.from(blockhashBytes), // 32 bytes
    compactU16(1), // 1 instruction
    Buffer.from([3]), // programIdIndex (tokenProgram)
    compactU16(3),
    Buffer.from([1, 2, 0]), // accounts: source, dest, authority
    compactU16(instructionData.length),
    instructionData,
  ]);
}

// --- Main: Pay and call API ---

async function callTTCWithPayment(endpoint) {
  const secretKey = new Uint8Array(/* your 64-byte secret key */);
  const keypair = nacl.sign.keyPair.fromSecretKey(secretKey);
  const walletAddress = bs58.encode(keypair.publicKey);

  // 1. Derive ATAs
  const senderATA = getATA(walletAddress, USDC_MINT);
  const treasuryATA = getATA(TREASURY, USDC_MINT);

  // 2. Build, sign, and send USDC transfer
  const {
    value: { blockhash },
  } = await rpc("getLatestBlockhash", [{ commitment: "confirmed" }]);
  const message = buildTransactionMessage(
    keypair.publicKey,
    senderATA,
    treasuryATA,
    blockhash,
  );
  const sig = nacl.sign.detached(new Uint8Array(message), keypair.secretKey);
  const txBytes = Buffer.concat([compactU16(1), Buffer.from(sig), message]);

  const signature = await rpc("sendTransaction", [
    txBytes.toString("base64"),
    { encoding: "base64", preflightCommitment: "confirmed" },
  ]);

  // 3. Wait for confirmation
  for (let i = 0; i < 30; i++) {
    const status = await rpc("getSignatureStatuses", [[signature]]);
    const st = status?.value?.[0];
    if (st?.err)
      throw new Error(`Transaction failed: ${JSON.stringify(st.err)}`);
    if (
      st?.confirmationStatus === "confirmed" ||
      st?.confirmationStatus === "finalized"
    )
      break;
    await new Promise((r) => setTimeout(r, 1000));
  }

  // 4. Build payment header and call API
  const payment = {
    signature,
    payer: walletAddress,
    amount: "50000",
    timestamp: Math.floor(Date.now() / 1000),
    payTo: TREASURY,
  };
  const header =
    "x402:" + Buffer.from(JSON.stringify(payment)).toString("base64");
  const res = await fetch(`https://ttc.box${endpoint}`, {
    headers: { "x-x402-payment": header },
  });
  return res.json();
}

const news = await callTTCWithPayment("/api/v1/markets/news");
```
