SDKs
Vercel AI SDK Provider
Build AI agents with USDC-SPL payments on Solana via x402 protocol
This page will be picked up by the docs.solvela.ai migration when it runs.
Installation
⚠️ Not yet published to npm.
@solvela/ai-sdk-provideris at0.2.1inpackage.jsonbut has not been published to the registry. The npm install below will fail today. To use the provider now, install from a local checkout of the monorepo:git clone https://github.com/solvela-ai/solvela cd solvela/sdks/ai-sdk-provider && npm install && npm run build
Once published, the canonical install will be:
npm install @solvela/ai-sdk-provider ai @ai-sdk/provider-utils @ai-sdk/openai-compatibleNode >= 18.17 required. ESM only.
Overview
The @solvela/ai-sdk-provider package integrates Solvela's x402 payment protocol with Vercel's AI SDK. Every LLM API call is backed by a signed Solana USDC-SPL transaction. Agents specify a wallet adapter; the provider handles payment verification and budget tracking automatically.
import { generateText } from 'ai';
import { createSolvelaProvider } from '@solvela/ai-sdk-provider';
const solvela = createSolvelaProvider({
baseURL: 'https://api.solvela.ai/v1',
wallet: yourWalletAdapter, // required
});
const { text } = await generateText({
model: solvela('anthropic-claude-sonnet-4-5'),
prompt: 'Explain Solana validators.',
});Authentication
Every request requires a wallet adapter implementing SolvelaWalletAdapter. The adapter is responsible for signing USDC-SPL transactions that serve as proof of payment.
SolvelaWalletAdapter interface
interface SolvelaWalletAdapter {
readonly label: string; // e.g., "phantom", "ledger", "mpc-signer"
signPayment(args: {
paymentRequired: SolvelaPaymentRequired;
resourceUrl: string;
requestBody: string;
signal?: AbortSignal;
}): Promise<string>; // base64-encoded PAYMENT-SIGNATURE header
}Development: createLocalWalletAdapter
For development and testing, use the built-in local adapter:
import { createLocalWalletAdapter } from '@solvela/ai-sdk-provider/adapters/local';
import { Keypair } from '@solana/web3.js';
import bs58 from 'bs58';
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_WALLET_KEY!));
const adapter = createLocalWalletAdapter(keypair);
const solvela = createSolvelaProvider({
wallet: adapter,
});Warning: Never use createLocalWalletAdapter in production. It stores the private key in memory unencrypted. The package only exports it from the ./adapters/local sub-export tree; browser builds are safe.
Production: Custom Adapter
Implement SolvelaWalletAdapter with your infrastructure (hardware wallet, MPC service, remote signer, etc.):
import type { SolvelaWalletAdapter, SolvelaPaymentRequired } from '@solvela/ai-sdk-provider';
class MyHardwareWalletAdapter implements SolvelaWalletAdapter {
readonly label = 'ledger-solana';
async signPayment(args: {
paymentRequired: SolvelaPaymentRequired;
resourceUrl: string;
requestBody: string;
}): Promise<string> {
const { amount, pay_to } = args.paymentRequired.accepts[0];
// 1. Build USDC-SPL transaction to `pay_to` for `amount` atomic units
// 2. Sign with your Ledger / hardware wallet
// 3. Return base64-encoded signed transaction as PAYMENT-SIGNATURE
const signedTx = await this.ledger.signSolanaTransaction(tx);
return Buffer.from(signedTx).toString('base64');
}
}
const solvela = createSolvelaProvider({
wallet: new MyHardwareWalletAdapter(),
});MPC / Remote Signer Skeleton
For distributed signing (MPC, Fireblocks, etc.):
class RemoteSignerAdapter implements SolvelaWalletAdapter {
readonly label = 'mpc-signer';
async signPayment(args: {
paymentRequired: SolvelaPaymentRequired;
resourceUrl: string;
requestBody: string;
}): Promise<string> {
const { amount, pay_to } = args.paymentRequired.accepts[0];
// 1. Call your remote signing service with transaction details
// 2. MPC nodes sign threshold-of-N shares
// 3. Aggregate into full signature
const response = await fetch('https://your-mpc-service/sign', {
method: 'POST',
body: JSON.stringify({
to: pay_to,
amount,
network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
}),
});
const { signedTx } = await response.json();
return signedTx; // already base64
}
}Configuration
const solvela = createSolvelaProvider({
baseURL: 'https://api.solvela.ai/v1', // default — optional
wallet: myWalletAdapter, // REQUIRED
apiKey: 'sk_...', // optional scope token
headers: { /* ... */ }, // optional static headers
sessionBudget: BigInt(100_000_000), // optional: $100 USDC cap
supportsStructuredOutputs: true, // default: false
});| Option | Type | Default | Description |
|---|---|---|---|
baseURL | string | https://api.solvela.ai/v1 | Gateway endpoint. Resolves from SOLVELA_API_URL env var if not set. |
wallet | SolvelaWalletAdapter | — | REQUIRED. Adapter implementation for signing. |
apiKey | string | undefined | Optional scope token forwarded as Authorization header. Not a signing key. |
headers | Record<string, string> | {} | Static headers merged into every request. PAYMENT-SIGNATURE is filtered. |
sessionBudget | bigint | undefined | USDC atomic units cap. Throws SolvelaBudgetExceededError when exceeded. |
supportsStructuredOutputs | boolean | false | Enable schema validation via responseFormat. |
maxBodyBytes | number | 1000000 | Cap on request body size (bytes) before signing. Oversized bodies throw SolvelaPaymentError. |
allowInsecureBaseURL | boolean | false | Allow non-HTTPS baseURL (dev/test only). |
Streaming
Stream responses token-by-token:
import { streamText } from 'ai';
const result = streamText({
model: solvela('anthropic-claude-sonnet-4-5'),
prompt: 'Stream 100 creative names for a Solana validator.',
});
for await (const chunk of result.fullStream) {
if (chunk.type === 'text-delta') {
process.stdout.write(chunk.delta);
}
}The x402 payment is signed once before streaming begins. If payment verification fails, you receive an error before the stream opens.
Tool calls
Use tool definitions with your chosen model:
import { generateText, tool } from 'ai';
import { z } from 'zod';
import { createSolvelaProvider } from '@solvela/ai-sdk-provider';
import { createLocalWalletAdapter } from '@solvela/ai-sdk-provider/adapters/local';
const solvela = createSolvelaProvider({ wallet: adapter });
const { toolResults } = await generateText({
model: solvela('anthropic-claude-sonnet-4-5'),
tools: {
getPrice: tool({
description: 'Get token price',
inputSchema: z.object({ token: z.string() }),
}),
},
prompt: 'What is SOL worth?',
});Tools are called via the Solvela gateway; each call triggers a new x402 payment for the tool invocation.
Structured output
Opt-in schema validation (disabled by default):
import { generateObject } from 'ai';
import { z } from 'zod';
import { createSolvelaProvider } from '@solvela/ai-sdk-provider';
const solvela = createSolvelaProvider({ wallet: adapter, supportsStructuredOutputs: true });
const { object } = await generateObject({
model: solvela('anthropic-claude-sonnet-4-5'),
schema: z.object({
name: z.string(),
founded: z.number(),
}),
prompt: 'Describe Solana in JSON.',
});
console.log(object.name, object.founded);Set supportsStructuredOutputs: true in provider settings to enable. Not all models support schemas; consult the model registry.
Session budgets
Reserve USDC at provider creation; debit on successful requests:
const solvela = createSolvelaProvider({
wallet: adapter,
sessionBudget: BigInt(50_000_000), // $50 USDC
});
try {
await generateText({
model: solvela('anthropic-claude-sonnet-4-5'),
prompt: 'The cost of this request is deducted from the $50 budget.',
});
} catch (e) {
if (e instanceof SolvelaBudgetExceededError) {
console.error('Session budget exhausted. Increase sessionBudget to retry.');
}
}If no sessionBudget is set, spending is unlimited. Each request deducts its cost from the reserved amount; if the next request would exceed the remaining balance, a SolvelaBudgetExceededError is thrown before payment is signed.
Error reference
All Solvela API errors extend APICallError from @ai-sdk/provider. SolvelaInvalidConfigError extends AISDKError instead, as it is a construction-time validation error with no associated HTTP request. Use .isInstance() for type-safe error checking:
import {
SolvelaPaymentError,
SolvelaBudgetExceededError,
SolvelaSigningError,
SolvelaUpstreamError,
SolvelaInvalidConfigError,
} from '@solvela/ai-sdk-provider';
try {
await generateText({ model: solvela('anthropic-claude-sonnet-4-5'), prompt: '...' });
} catch (error) {
if (SolvelaPaymentError.isInstance(error)) {
console.error('Payment verification failed:', error.message);
// isRetryable: false — build a new payment
} else if (SolvelaBudgetExceededError.isInstance(error)) {
console.error('Budget exceeded');
// isRetryable: false — increase sessionBudget or defer request
} else if (SolvelaSigningError.isInstance(error)) {
console.error('Wallet could not sign:', error.message);
// isRetryable: false — check adapter configuration
} else if (SolvelaUpstreamError.isInstance(error)) {
console.error('LLM provider error:', error.statusCode);
// isRetryable: true if statusCode is 5xx, 408, 409, 429, or undefined (network)
if (error.isRetryable) {
// safe to retry
}
} else if (SolvelaInvalidConfigError.isInstance(error)) {
console.error('Configuration error:', error.message);
// isRetryable: false — fix provider settings
}
}| Error | Retryable | When | Fix |
|---|---|---|---|
SolvelaPaymentError | No | Payment signature verification failed on-chain | Build a new payment; check Solana network |
SolvelaBudgetExceededError | No | Session spending limit reached | Increase sessionBudget or create new provider instance |
SolvelaSigningError | No | Wallet adapter failed to sign | Check wallet configuration; verify keypair/signer access |
SolvelaUpstreamError | Conditional | LLM provider returned error | Retry if isRetryable === true; check provider status |
SolvelaInvalidConfigError | No | Provider settings validation failed at construction | Fix baseURL, wallet, sessionBudget, or other settings |
Observability integration
Integrate with Sentry to capture errors while redacting sensitive data:
import * as Sentry from '@sentry/node';
import { SolvelaPaymentError, SolvelaUpstreamError } from '@solvela/ai-sdk-provider';
Sentry.init({
dsn: 'https://...',
beforeSend(event, hint) {
const error = hint.originalException;
// Strip PAYMENT-SIGNATURE from error details
if (error instanceof Error && error.message) {
error.message = error.message.replace(/PAYMENT-SIGNATURE[=:]\S+/gi, 'PAYMENT-SIGNATURE=***');
}
if (error && typeof error === 'object' && 'responseHeaders' in error) {
const headers = error.responseHeaders as Record<string, string>;
delete headers['payment-signature'];
delete headers['PAYMENT-SIGNATURE'];
}
return event;
},
});
// Sentry now captures Solvela errors with payment details sanitizedSee Sentry Node documentation for full integration patterns.
Migration from openai SDK
If you're currently using @ai-sdk/openai:
// Before: @ai-sdk/openai
import { openai } from '@ai-sdk/openai';
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: '...',
});
// After: @solvela/ai-sdk-provider
import { createSolvelaProvider } from '@solvela/ai-sdk-provider';
import { createLocalWalletAdapter } from '@solvela/ai-sdk-provider/adapters/local';
const solvela = createSolvelaProvider({
wallet: createLocalWalletAdapter(keypair),
});
const { text } = await generateText({
model: solvela('openai-gpt-4o'),
prompt: '...',
});Solvela prefixes model IDs with their provider namespace (e.g., openai-gpt-4o, anthropic-claude-sonnet-4-5). See the registry at @solvela/ai-sdk-provider's MODELS export for the full list.
All generateText, generateObject, streamText, etc. methods work identically. The only new requirement is the wallet adapter.
Environment variables
| Variable | Type | Default | Description |
|---|---|---|---|
SOLVELA_API_URL | string | https://api.solvela.ai/v1 | Gateway base URL. Overridden by baseURL setting. |
SOLVELA_SESSION_BUDGET | string | undefined | USDC atomic units budget (env var alternative to sessionBudget setting). |
SOLVELA_MAX_SIGNED_BODY_BYTES | string | 1000000 | Cap on request body size before signing (bytes). |
SOLVELA_ALLOW_INSECURE_BASE_URL | string | false | Allow non-HTTPS baseURL in non-production (ignored in prod/Edge). |
SOLVELA_AI_SDK_PROVIDER_TEST_MODE | string | false | Enable test mode for localhost integration tests. |
NODE_ENV | string | — | Set to test to unlock test-mode conditions. |
Adapters may define additional environment variables (e.g., SOLANA_WALLET_KEY for the local adapter).
Troubleshooting
"Invalid provider configuration"
SolvelaInvalidConfigError: [solvela] The default `solvela` singleton has no wallet configured.You imported the default singleton without a wallet. Use createSolvelaProvider({ wallet: ... }) instead:
// Wrong
import { solvela } from '@solvela/ai-sdk-provider';
const model = solvela('anthropic-claude-sonnet-4-5'); // throws
// Right
import { createSolvelaProvider } from '@solvela/ai-sdk-provider';
const solvela = createSolvelaProvider({ wallet: myAdapter });
const model = solvela('anthropic-claude-sonnet-4-5');"PAYMENT-SIGNATURE invalid"
The wallet adapter returned a malformed signature, or the transaction was rejected on-chain. Verify:
- The adapter correctly signs USDC-SPL transfers to the
pay_toaddress. - The amount matches
paymentRequired.accepts[0].amountexactly. - The Solana network matches the configured RPC (mainnet-beta by default).
"Budget exceeded"
SolvelaBudgetExceededError: Session budget exhaustedThe request would exceed sessionBudget. Either increase the budget, create a new provider instance, or reduce request scope (e.g., lower max_tokens).
"Upstream error 5xx"
The LLM provider is experiencing issues. The error has isRetryable: true; use exponential backoff to retry:
import { SolvelaUpstreamError } from '@solvela/ai-sdk-provider';
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await generateText({ /* ... */ });
} catch (error) {
if (SolvelaUpstreamError.isInstance(error) && error.isRetryable) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
continue;
}
throw error;
}
}Browser / Edge Runtime Error
The @solvela/ai-sdk-provider/adapters/local sub-export does not ship to browsers or Edge Runtime; tree-shaking prevents accidental inclusion. If you see a module not found error, you're trying to use the local adapter in a browser context. Use a custom adapter instead.
FAQ
Q: Is my private key ever sent to Solvela?
No. The adapter signs transactions locally. Only the base64-encoded signed transaction (PAYMENT-SIGNATURE header) is sent to the gateway.
Q: Can I use the same wallet for multiple models?
Yes. A single adapter can power multiple model instances. Each request is signed independently.
Q: What happens if a request fails after payment is signed?
The USDC is deducted from your wallet. The request is not retried; you must retry manually. If a request times out mid-stream, payment was already signed.
Q: How do I support multiple wallet types (Phantom, Ledger, etc.)?
Implement SolvelaWalletAdapter for each wallet type, then route at runtime:
function getAdapter(walletType: 'phantom' | 'ledger'): SolvelaWalletAdapter {
if (walletType === 'phantom') {
return new PhantomAdapter();
} else {
return new LedgerAdapter();
}
}Q: Can I disable payment for local testing?
No. Every request requires a valid wallet adapter. For testing, use createLocalWalletAdapter with a test keypair and a local / staging gateway.
Q: What model IDs are available?
See the Models reference or query the /v1/models endpoint (no auth required — model discovery is public):
curl https://api.solvela.ai/v1/models