Payments

Solana wallet authentication, x402 USDC payments, and Polar credit card top-ups.

Wallet Authentication

Every protected Setu endpoint requires Solana wallet authentication. Instead of API keys, you sign a timestamp nonce with your wallet's Ed25519 private key and send three headers:

HeaderValue
x-wallet-addressYour Solana public key (base58)
x-wallet-signatureEd25519 signature of the nonce (base58)
x-wallet-nonceCurrent timestamp in milliseconds (e.g. 1706000000000)

How Signing Works

  1. Generate a nonce: the current Date.now() timestamp as a string
  2. Encode the nonce as UTF-8 bytes
  3. Sign those bytes with nacl.sign.detached() using the wallet's secret key
  4. Base58-encode the 64-byte signature
import nacl from "tweetnacl";
import bs58 from "bs58";
import { Keypair } from "@solana/web3.js";

// Load wallet from base58 private key
const privateKeyBytes = bs58.decode(process.env.SETU_PRIVATE_KEY);
const keypair = Keypair.fromSecretKey(privateKeyBytes);

function createAuthHeaders() {
  const nonce = Date.now().toString();
  const message = new TextEncoder().encode(nonce);
  const signature = nacl.sign.detached(message, keypair.secretKey);

  return {
    "x-wallet-address": keypair.publicKey.toBase58(),
    "x-wallet-signature": bs58.encode(signature),
    "x-wallet-nonce": nonce,
  };
}

Server-Side Verification

The server verifies each request by:

  1. Decoding the public key from x-wallet-address to bytes
  2. Decoding the signature from x-wallet-signature via base58
  3. Encoding the nonce string to UTF-8 bytes
  4. Calling nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes)
  5. Checking the nonce is within 60 seconds of server time

If the nonce is older than 60 seconds, the server returns 401 Nonce expired.

Wallet Key Format

The SETU_PRIVATE_KEY is a base58-encoded Solana secret key (64 bytes when decoded). The first 32 bytes are the private scalar and the last 32 bytes are the public key. This is the standard Solana keypair format.

# Generate a new wallet
import { Keypair } from "@solana/web3.js";
import bs58 from "bs58";

const keypair = Keypair.generate();
const privateKey = bs58.encode(keypair.secretKey);     // store this
const publicKey = keypair.publicKey.toBase58();         // your wallet address

// Import an existing key
const imported = Keypair.fromSecretKey(bs58.decode(privateKey));

x402 Payment Protocol

Setu uses the x402 protocol for on-chain USDC payments. The x402 protocol extends HTTP 402 (Payment Required) into a machine-readable payment flow.

How It Works

  1. Client makes an API request (e.g. POST /v1/messages)
  2. Server checks balance — if below $0.05, returns HTTP 402
  3. 402 response includes accepts array — each entry describes a USDC payment option with amount, network, asset, and destination
  4. Client picks a payment option and builds a signed USDC transfer transaction using the x402 SDK
  5. Client submits to /v1/topup with the signed transaction and the chosen payment requirement
  6. Server settles via facilitator — the payment is submitted to the x402 facilitator which handles on-chain confirmation
  7. Balance is credited and the client retries the original request

402 Response Format

{
  "x402Version": 1,
  "error": {
    "message": "Balance too low. Please top up.",
    "type": "insufficient_balance",
    "current_balance": "0.00",
    "minimum_balance": "0.05",
    "topup_required": true
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "solana",
      "maxAmountRequired": "5000000",
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "payTo": "<platform-wallet>",
      "resource": "https://api.setu.ottocode.io/v1/responses",
      "description": "Top-up required for API access (5.00 USD)",
      "mimeType": "application/json",
      "maxTimeoutSeconds": 60,
      "extra": { "feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4" }
    }
  ]
}

The maxAmountRequired is in micro-USDC (1 USDC = 1,000,000). The extra.feePayer is the facilitator's fee payer account that covers Solana transaction fees.

Building the Payment Transaction

Use the x402 npm package to create a signed payment transaction:

import { createPaymentHeader } from "x402/client";
import { svm } from "x402/shared";
import bs58 from "bs58";
import { Buffer } from "node:buffer";

const requirement = accepts[0]; // pick from 402 response

// Create a signer from your wallet's private key
const signer = await svm.createSignerFromBase58(
  bs58.encode(keypair.secretKey)
);

// Build the signed payment header
const header = await createPaymentHeader(
  signer,
  1, // x402 version
  requirement,
  { svmConfig: { rpcUrl: "https://api.mainnet-beta.solana.com" } }
);

// Decode the header to get the signed transaction
const decoded = JSON.parse(
  Buffer.from(header, "base64").toString("utf-8")
);

const paymentPayload = {
  x402Version: 1,
  scheme: "exact",
  network: requirement.network,
  payload: { transaction: decoded.payload.transaction },
};

Submitting the Top-up

const response = await fetch("https://api.setu.ottocode.io/v1/topup", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    ...createAuthHeaders(),
  },
  body: JSON.stringify({
    paymentPayload,
    paymentRequirement: requirement,
  }),
});

const result = await response.json();
// {
//   success: true,
//   amount: 5,
//   new_balance: "5.00000000",
//   transaction: "5Kf8mG..."
// }

Duplicate Protection

Submitting the same signed transaction twice returns { success: true, duplicate: true } without double-crediting. This makes retries safe.

Settlement Flow

The facilitator at https://facilitator.payai.network/settle receives the signed transaction and payment requirement, submits it to Solana, waits for on-chain confirmation, and returns the result. The facilitator also acts as the fee payer so the user doesn't need SOL for transaction fees.


Polar Payments (Credit Card)

For users who prefer credit card payments, Setu integrates with Polar as a fiat on-ramp.

How It Works

  1. Client calls POST /v1/topup/polar with desired credit amount and success URL
  2. Setu creates a Polar checkout session with the charge amount (credit + processing fees)
  3. Client redirects user to the Polar checkout URL
  4. After payment, Polar sends a webhook to Setu
  5. Setu verifies the webhook signature, confirms the payment, and credits the balance

Fee Structure

Polar payments include processing fees to cover the credit amount:

Fee ComponentRate
Base fee4%
International fee1.5%
Fixed fee$0.40

The charge amount is calculated so that after fees, the full credit amount is received:

chargeAmount = ceil((creditAmount + $0.40) / (1 - 0.055))

Create Checkout

POST /v1/topup/polar
Content-Type: application/json

{
  "amount": 10,
  "successUrl": "https://myapp.com/topup/success"
}

// Response:
{
  "success": true,
  "checkoutId": "polar_checkout_...",
  "checkoutUrl": "https://polar.sh/checkout/...",
  "creditAmount": 10,
  "chargeAmount": 11.03,
  "feeAmount": 1.03
}

Estimate Fees

GET /v1/topup/polar/estimate?amount=10

{
  "creditAmount": 10,
  "chargeAmount": 11.03,
  "feeAmount": 1.03,
  "feeBreakdown": {
    "basePercent": 4,
    "internationalPercent": 1.5,
    "fixedCents": 40
  }
}

Check Status

GET /v1/topup/polar/status?checkoutId=polar_checkout_...

{
  "checkoutId": "polar_checkout_...",
  "confirmed": true,
  "amountUsd": 10,
  "confirmedAt": "2026-01-20T10:00:00.000Z"
}

Limits

LimitValue
Minimum top-up$5
Maximum top-up$500

Cost Tracking

Setu tracks per-request costs and returns them in two ways depending on whether the response is streaming.

Non-Streaming

Cost metadata is returned in response headers:

x-cost-usd: 0.00001234
x-balance-remaining: 4.99998766

Streaming

Cost metadata is injected as an SSE comment at the end of the stream:

: setu {"cost_usd":"0.00000904","balance_remaining":"4.99856275","input_tokens":20,"output_tokens":11}

Parse it by looking for lines starting with : setu :

for (const line of chunk.split("\n")) {
  if (line.startsWith(": setu ")) {
    const data = JSON.parse(line.slice(7));
    console.log("Cost:", data.cost_usd, "Balance:", data.balance_remaining);
  }
}

Pricing Formula

Cost per request is calculated from the model's per-million-token rates:

cost = (inputTokens / 1M × inputRate
      + cachedReadTokens / 1M × cacheReadRate
      + cacheWriteTokens / 1M × cacheWriteRate
      + outputTokens / 1M × outputRate) × 1.005

The 1.005 multiplier is the 0.5% markup.

Balance Endpoint

GET /v1/balance
Headers: x-wallet-address, x-wallet-signature, x-wallet-nonce

{
  "wallet_address": "ABC123...",
  "balance_usd": 4.95,
  "total_spent": 0.05,
  "total_topups": 5.00,
  "request_count": 10,
  "created_at": "2026-01-20T10:00:00.000Z",
  "last_request": "2026-01-24T15:30:00.000Z"
}