Integration Guide
Integrate Setu into your application using the @ottocode/ai-sdk package or raw HTTP.
Using @ottocode/ai-sdk
The recommended way to integrate Setu. The SDK handles wallet authentication, automatic 402 payment handling, provider routing, and Anthropic prompt caching out of the box.
For comprehensive SDK documentation, see the AI SDK docs.
Install
bun add @ottocode/ai-sdk ai
# or
npm install @ottocode/ai-sdk aiQuick Start
Create a Setu instance with createSetu() and call setu.model() to get an ai-sdk compatible model. The SDK auto-resolves which provider to use based on the model name.
import { createSetu } from "@ottocode/ai-sdk";
import { generateText } from "ai";
const setu = createSetu({
auth: { privateKey: process.env.SETU_PRIVATE_KEY! },
});
const { text } = await generateText({
model: setu.model("claude-sonnet-4-6"),
prompt: "Hello!",
});
console.log(text);Provider Auto-Resolution
Models are resolved to providers by prefix — no need to specify providerNpm manually:
| Prefix | Provider | API Format |
|---|---|---|
claude- | Anthropic | /v1/messages |
gpt-, o1, o3, o4, codex- | OpenAI | /v1/responses |
gemini- | Google native | |
kimi- | Moonshot | /v1/chat/completions |
MiniMax- | MiniMax | /v1/messages |
z1- | Z.AI | /v1/chat/completions |
setu.model("claude-sonnet-4-6"); // → anthropic
setu.model("gpt-5"); // → openai
setu.model("gemini-3-flash-preview"); // → google
setu.model("kimi-k2.5"); // → moonshotExplicit Provider
Override auto-resolution when needed:
const model = setu.provider("openai").model("gpt-5");
const model = setu.provider("anthropic", "anthropic-messages").model("claude-sonnet-4-6");Streaming
import { streamText } from "ai";
const result = streamText({
model: setu.model("claude-sonnet-4-6"),
prompt: "Write a short story about a robot.",
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}Tool Calling
import { generateText, tool } from "ai";
import { z } from "zod";
const { text } = await generateText({
model: setu.model("claude-sonnet-4-6"),
prompt: "What's the weather in Tokyo?",
tools: {
getWeather: tool({
description: "Get weather for a location",
parameters: z.object({
location: z.string(),
}),
execute: async ({ location }) => {
return { temperature: 22, condition: "cloudy" };
},
}),
},
});Anthropic Prompt Caching
The SDK automatically injects cache_control on the first system block and last message for Anthropic models, saving ~90% on cached token costs. You can customize or disable this:
// Default: auto caching (1 system + 1 message breakpoint)
createSetu({ auth });
// Disable completely
createSetu({ auth, cache: { anthropicCaching: false } });
// Manual: SDK won't inject cache_control — set it yourself
createSetu({ auth, cache: { anthropicCaching: { strategy: "manual" } } });
// Custom breakpoint count
createSetu({
auth,
cache: {
anthropicCaching: {
systemBreakpoints: 2,
messageBreakpoints: 3,
messagePlacement: "last",
},
},
});Extended Thinking (Anthropic)
const { text } = await generateText({
model: setu.model("claude-sonnet-4-6"),
prompt: "Solve this complex math problem...",
providerOptions: {
anthropic: {
thinking: { type: "enabled", budgetTokens: 16000 },
},
},
});Configuration
const setu = createSetu({
// Required: Solana wallet private key (base58)
auth: { privateKey: "..." },
// Optional: Setu API base URL (default: https://api.setu.ottocode.io)
baseURL: "https://api.setu.ottocode.io",
// Optional: Solana RPC URL (default: https://api.mainnet-beta.solana.com)
rpcURL: "https://api.mainnet-beta.solana.com",
// Optional: Payment callbacks (see below)
callbacks: { /* ... */ },
// Optional: Cache configuration
cache: { /* ... */ },
// Optional: Payment options
payment: {
topupApprovalMode: "auto", // "auto" | "approval"
autoPayThresholdUsd: 5.0,
maxRequestAttempts: 3,
maxPaymentAttempts: 20,
},
// Optional: Custom model→provider mappings
modelMap: { "my-custom-model": "openai" },
// Optional: Register custom providers
providers: [
{ id: "my-provider", apiFormat: "openai-chat", modelPrefix: "myp-" },
],
});External Signer (No Private Key)
Instead of sharing your private key, provide callback functions for signing. The SDK builds transactions and hands you raw bytes — you sign them however you want. Works with any wallet, framework, or HSM.
import { createSetu } from "@ottocode/ai-sdk";
import { generateText } from "ai";
const setu = createSetu({
auth: {
signer: {
walletAddress: "YOUR_SOLANA_PUBLIC_KEY",
// Required: sign auth nonces
signNonce: async (nonce) => {
return await myWallet.signMessage(nonce);
},
// Optional: sign payment transactions (raw bytes in, signed bytes out)
signTransaction: async (transaction) => {
return await myWallet.signTransaction(transaction);
},
},
},
});
const { text } = await generateText({
model: setu.model("claude-sonnet-4-6"),
prompt: "Hello!",
});With Phantom / Wallet Adapter
const setu = createSetu({
auth: {
signer: {
walletAddress: wallet.publicKey.toBase58(),
signNonce: async (nonce) => {
const encoded = new TextEncoder().encode(nonce);
const { signature } = await wallet.signMessage(encoded);
return bs58.encode(signature);
},
signTransaction: async (txBytes) => {
const tx = Transaction.from(txBytes);
const signed = await wallet.signTransaction(tx);
return signed.serialize();
},
},
},
});Auth-Only (No Payments)
If you only need request authentication and don't need auto-topup, you can omit signTransaction. Payments will throw a clear error if attempted:
const setu = createSetu({
auth: {
signer: {
walletAddress: "YOUR_SOLANA_PUBLIC_KEY",
signNonce: async (nonce) => {
return await myWallet.signMessage(nonce);
},
},
},
});Payment Callbacks
const setu = createSetu({
auth: { privateKey: "..." },
callbacks: {
onPaymentRequired: (amountUsd, currentBalance) => {
console.log(`Payment required: $${amountUsd}`);
},
onPaymentSigning: () => {
console.log("Signing payment...");
},
onPaymentComplete: ({ amountUsd, newBalance, transactionId }) => {
console.log(`Paid $${amountUsd}, balance: $${newBalance}`);
},
onPaymentError: (error) => {
console.error("Payment failed:", error);
},
onBalanceUpdate: ({ costUsd, balanceRemaining, inputTokens, outputTokens }) => {
console.log(`Cost: $${costUsd}, remaining: $${balanceRemaining}`);
},
onPaymentApproval: async ({ amountUsd, currentBalance }) => {
// return "crypto" to pay, "fiat" for fiat flow, "cancel" to abort
return "crypto";
},
},
});| Callback | When |
|---|---|
onPaymentRequired(amountUsd, currentBalance) | 402 received, payment about to start |
onPaymentSigning() | Building and signing the USDC transaction |
onPaymentComplete({ amountUsd, newBalance, transactionId }) | Payment settled successfully |
onPaymentError(error) | Payment failed |
onPaymentApproval({ amountUsd, currentBalance }) | Approval mode: asks user to approve/cancel/choose fiat |
onBalanceUpdate({ costUsd, balanceRemaining }) | After each request with cost info (streaming & non-streaming) |
Balance
// Setu account balance
const balance = await setu.balance();
// { walletAddress, balance, totalSpent, totalTopups, requestCount }
// On-chain USDC balance
const wallet = await setu.walletBalance("mainnet");
// { walletAddress, usdcBalance, network }
// Wallet address
console.log(setu.walletAddress);Low-Level: Custom Fetch
Use the x402-aware fetch wrapper directly for full control:
const customFetch = setu.fetch();
const response = await customFetch(
"https://api.setu.ottocode.io/v1/messages",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "Hello" }],
max_tokens: 1024,
}),
}
);Standalone Utilities
import {
fetchBalance,
fetchWalletUsdcBalance,
getPublicKeyFromPrivate,
addAnthropicCacheControl,
createSetuFetch,
} from "@ottocode/ai-sdk";
// Get wallet address from private key
const address = getPublicKeyFromPrivate(privateKey);
// Fetch balance without creating a full Setu instance
const balance = await fetchBalance({ privateKey });
// Fetch on-chain USDC
const usdc = await fetchWalletUsdcBalance({ privateKey }, "mainnet");Raw HTTP Integration
You can integrate Setu without the SDK by making direct HTTP requests.
Endpoint Reference
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/ | GET | No | Service info and available endpoints |
/health | GET | No | Health check |
/v1/models | GET | No | List all models with pricing |
/v1/balance | GET | Yes | Check wallet balance |
/v1/topup | POST | Yes | Top up via x402 USDC payment |
/v1/topup/polar | POST | Yes | Create Polar credit card checkout |
/v1/responses | POST | Yes | OpenAI Responses API (passthrough) |
/v1/messages | POST | Yes | Anthropic Messages API (passthrough) |
/v1/chat/completions | POST | Yes | OpenAI-compatible (Moonshot, Z.AI) |
/v1/models/{model}:generateContent | POST | Yes | Google native Generative AI |
/v1/models/{model}:streamGenerateContent | POST | Yes | Google native streaming |
Example: Direct Anthropic Call
import nacl from "tweetnacl";
import bs58 from "bs58";
import { Keypair } from "@solana/web3.js";
const keypair = Keypair.fromSecretKey(
bs58.decode(process.env.SETU_PRIVATE_KEY)
);
const nonce = Date.now().toString();
const signature = nacl.sign.detached(
new TextEncoder().encode(nonce),
keypair.secretKey
);
const response = await fetch(
"https://api.setu.ottocode.io/v1/messages",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-wallet-address": keypair.publicKey.toBase58(),
"x-wallet-signature": bs58.encode(signature),
"x-wallet-nonce": nonce,
},
body: JSON.stringify({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [
{ role: "user", content: "Hello, Claude!" }
],
}),
}
);
if (response.status === 402) {
// Handle payment — see Payments docs
}
const data = await response.json();
console.log(data.content[0].text);Error Handling
| Status | Meaning | Action |
|---|---|---|
400 | Invalid model, missing fields, unsupported amount | Fix request |
401 | Missing auth headers, invalid signature, expired nonce | Re-sign with fresh nonce |
402 | Balance below $0.05 | Handle payment via x402 or Polar |
429 | Upstream provider rate limited | Retry with backoff |
503 | Upstream provider overloaded or quota issue | Retry later |
500 | Server error | Report issue |
Environment Variables
Client-Side
| Variable | Required | Description |
|---|---|---|
SETU_PRIVATE_KEY | Yes* | Base58-encoded Solana private key (* not required when using external signer) |
SETU_BASE_URL | No | Override API URL (default: https://api.setu.ottocode.io) |
SETU_SOLANA_RPC_URL | No | Custom Solana RPC (default: https://api.mainnet-beta.solana.com) |
Using with otto
otto has built-in Setu support. Set it as your provider:
# Login with Setu (generates or imports a Solana wallet)
otto auth login setu
# Or set the private key directly
export SETU_PRIVATE_KEY="your-base58-private-key"
# Use Setu as default provider
otto setup # select "setu"
# Or per-request
otto ask "hello" --provider setu --model claude-sonnet-4-6otto uses @ottocode/ai-sdk under the hood via createSetu(), handling all wallet auth and payment flows transparently.