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 ai

Quick 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:

PrefixProviderAPI Format
claude-Anthropic/v1/messages
gpt-, o1, o3, o4, codex-OpenAI/v1/responses
gemini-GoogleGoogle 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");              // → moonshot

Explicit 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";
    },
  },
});
CallbackWhen
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

EndpointMethodAuthDescription
/GETNoService info and available endpoints
/healthGETNoHealth check
/v1/modelsGETNoList all models with pricing
/v1/balanceGETYesCheck wallet balance
/v1/topupPOSTYesTop up via x402 USDC payment
/v1/topup/polarPOSTYesCreate Polar credit card checkout
/v1/responsesPOSTYesOpenAI Responses API (passthrough)
/v1/messagesPOSTYesAnthropic Messages API (passthrough)
/v1/chat/completionsPOSTYesOpenAI-compatible (Moonshot, Z.AI)
/v1/models/{model}:generateContentPOSTYesGoogle native Generative AI
/v1/models/{model}:streamGenerateContentPOSTYesGoogle 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

StatusMeaningAction
400Invalid model, missing fields, unsupported amountFix request
401Missing auth headers, invalid signature, expired nonceRe-sign with fresh nonce
402Balance below $0.05Handle payment via x402 or Polar
429Upstream provider rate limitedRetry with backoff
503Upstream provider overloaded or quota issueRetry later
500Server errorReport issue

Environment Variables

Client-Side

VariableRequiredDescription
SETU_PRIVATE_KEYYes*Base58-encoded Solana private key (* not required when using external signer)
SETU_BASE_URLNoOverride API URL (default: https://api.setu.ottocode.io)
SETU_SOLANA_RPC_URLNoCustom 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-6

otto uses @ottocode/ai-sdk under the hood via createSetu(), handling all wallet auth and payment flows transparently.