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:
| Header | Value |
|---|---|
x-wallet-address | Your Solana public key (base58) |
x-wallet-signature | Ed25519 signature of the nonce (base58) |
x-wallet-nonce | Current timestamp in milliseconds (e.g. 1706000000000) |
How Signing Works
- Generate a nonce: the current
Date.now()timestamp as a string - Encode the nonce as UTF-8 bytes
- Sign those bytes with
nacl.sign.detached()using the wallet's secret key - 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:
- Decoding the public key from
x-wallet-addressto bytes - Decoding the signature from
x-wallet-signaturevia base58 - Encoding the nonce string to UTF-8 bytes
- Calling
nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes) - 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
- Client makes an API request (e.g.
POST /v1/messages) - Server checks balance — if below
$0.05, returns HTTP402 - 402 response includes
acceptsarray — each entry describes a USDC payment option with amount, network, asset, and destination - Client picks a payment option and builds a signed USDC transfer transaction using the x402 SDK
- Client submits to
/v1/topupwith the signed transaction and the chosen payment requirement - Server settles via facilitator — the payment is submitted to the x402 facilitator which handles on-chain confirmation
- 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
- Client calls
POST /v1/topup/polarwith desired credit amount and success URL - Setu creates a Polar checkout session with the charge amount (credit + processing fees)
- Client redirects user to the Polar checkout URL
- After payment, Polar sends a webhook to Setu
- 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 Component | Rate |
|---|---|
| Base fee | 4% |
| International fee | 1.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
| Limit | Value |
|---|---|
| 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.99998766Streaming
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.005The 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"
}