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-provider is at 0.2.1 in package.json but 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-compatible

Node >= 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
});
OptionTypeDefaultDescription
baseURLstringhttps://api.solvela.ai/v1Gateway endpoint. Resolves from SOLVELA_API_URL env var if not set.
walletSolvelaWalletAdapterREQUIRED. Adapter implementation for signing.
apiKeystringundefinedOptional scope token forwarded as Authorization header. Not a signing key.
headersRecord<string, string>{}Static headers merged into every request. PAYMENT-SIGNATURE is filtered.
sessionBudgetbigintundefinedUSDC atomic units cap. Throws SolvelaBudgetExceededError when exceeded.
supportsStructuredOutputsbooleanfalseEnable schema validation via responseFormat.
maxBodyBytesnumber1000000Cap on request body size (bytes) before signing. Oversized bodies throw SolvelaPaymentError.
allowInsecureBaseURLbooleanfalseAllow 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
  }
}
ErrorRetryableWhenFix
SolvelaPaymentErrorNoPayment signature verification failed on-chainBuild a new payment; check Solana network
SolvelaBudgetExceededErrorNoSession spending limit reachedIncrease sessionBudget or create new provider instance
SolvelaSigningErrorNoWallet adapter failed to signCheck wallet configuration; verify keypair/signer access
SolvelaUpstreamErrorConditionalLLM provider returned errorRetry if isRetryable === true; check provider status
SolvelaInvalidConfigErrorNoProvider settings validation failed at constructionFix 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 sanitized

See 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

VariableTypeDefaultDescription
SOLVELA_API_URLstringhttps://api.solvela.ai/v1Gateway base URL. Overridden by baseURL setting.
SOLVELA_SESSION_BUDGETstringundefinedUSDC atomic units budget (env var alternative to sessionBudget setting).
SOLVELA_MAX_SIGNED_BODY_BYTESstring1000000Cap on request body size before signing (bytes).
SOLVELA_ALLOW_INSECURE_BASE_URLstringfalseAllow non-HTTPS baseURL in non-production (ignored in prod/Edge).
SOLVELA_AI_SDK_PROVIDER_TEST_MODEstringfalseEnable test mode for localhost integration tests.
NODE_ENVstringSet 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:

  1. The adapter correctly signs USDC-SPL transfers to the pay_to address.
  2. The amount matches paymentRequired.accepts[0].amount exactly.
  3. The Solana network matches the configured RPC (mainnet-beta by default).

"Budget exceeded"

SolvelaBudgetExceededError: Session budget exhausted

The 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