Bot & Agent Onboarding Guide
Nodius is purpose-built for automated systems. This guide covers the recommended onboarding flow, authentication method selection, endpoint-profile behavior, and hot-path optimization tips for bots, trading systems, and AI agents.
How Identity & Credits Work
Your Solana keypair is your identity. Credits are tied to your public key โ no signup, no email, no account creation step. Three things that sound interchangeable are actually separate:
| What you want to do | What it requires |
|---|---|
| Authenticate (prove you own the keypair) | A valid wallet signature. Always works, regardless of balance. |
Call account-info endpoints (GET /account/info, POST /account/api-key) |
An account record, which is created automatically on first deposit or by calling POST /account/api-key (zero balance). |
Make billable RPC calls (getSlot, getBalance, sendTransaction, etc.) |
A positive credit balance. Credits come from USDC deposits. |
If you call a billable method before funding, you'll get 402 Payment Required with deposit instructions. The signature is valid โ the credits aren't there yet.
Recommended Onboarding Flow
- Generate or load a Solana keypair (Ed25519)
- Send USDC (SPL) on Solana mainnet to the deposit address from that wallet (minimum $0.01) to buy credits โ the account record is created automatically and credits appear within seconds
- Start making RPC calls using wallet-sig auth โ the SDK handles signing automatically
- Generate an API key via
generateApiKey()(TypeScript) orgenerate_api_key()(Python). This hitsPOST /account/api-key, auto-stores the returned key, and subsequent calls use it automatically. If you are latency-sensitive, do this early โ API keys avoid per-request signing overhead.
generateApiKey()before funding: Creates the account record (zero balance) and returns an API key.GET /account/infowill work and return your deposit address. But billable RPC calls will still return402until the deposit lands. With x402 auto-pay enabled (autoPay: true/auto_pay=True), the first402triggers an automatic micro-deposit and the call is retried.
Minimal TypeScript flow
import { NodiusClient } from "@nodius/sdk";
// No @solana/web3.js import needed โ pass your base58 secret key directly
const rpc = NodiusClient.fromSecretKey("https://rpc.nodius.xyz", "your-base58-secret-key");
// Step 1: Fund โ send USDC (SPL) from this wallet to the deposit address.
// Credits appear within ~30 seconds of on-chain confirmation.
// Get the deposit address from the 402 response or:
const info = await rpc.getBillingAccount();
console.log("Deposit USDC to:", info.deposit_address);
// Step 2: Wait for credits to land, then make billable RPC calls:
const slot = await rpc.call("getSlot");
console.log("Slot:", slot);
// Step 3 (optional): Generate an API key for lower-latency auth:
const result = await rpc.generateApiKey();
console.log("API key:", result.api_key);
Minimal Python flow
from nodius import NodiusClient
# Accepts 32-byte seeds and 64-byte expanded keys in base58
client = NodiusClient.from_base58("https://rpc.nodius.xyz", secret_key="your-base58-secret-key")
# Step 1: Fund โ send USDC (SPL) from this wallet to the deposit address.
# Credits appear within ~30 seconds of on-chain confirmation.
# Get the deposit address from the 402 response or:
info = client.get_billing_account()
print("Deposit USDC to:", info.get("deposit_address"))
# Step 2: Wait for credits to land, then make billable RPC calls:
slot = client.call("getSlot")
print("Slot:", slot)
# Step 3 (optional): Generate an API key for lower-latency auth:
result = client.generate_api_key()
print("API key:", result["api_key"])
Alternative: explicit Keypair object (TypeScript)
import { Keypair } from "@solana/web3.js";
import { NodiusClient } from "@nodius/sdk";
const keypair = Keypair.generate();
const rpc = new NodiusClient("https://rpc.nodius.xyz", { keypair });
await rpc.generateApiKey(); // also auto-stores API key
API keys are auto-stored by generateApiKey() / generate_api_key(). The POST /account/api-key endpoint generates an API key and returns it. Manual key management is rarely needed:
Auth Methods Comparison
| Method | Latency | Security | Best For |
|---|---|---|---|
API Key (X-Api-Key header or ?api-key= URL) |
Lowest โ no per-request signing | Medium (secret storage) | Latency-sensitive bots, drop-in with standard Solana libraries |
| Per-request wallet sig | Low โ Ed25519 sign + SHA-256 hash + nonce check per request | Highest (no API key secret) | Security-critical automated systems |
| Session token (Bearer) | Low โ single Redis lookup | Medium (1h expiry) | WebSocket connections, dApps |
All three modes work for every request type. The difference is latency and convenience. Wallet signatures are the zero-setup default โ the SDK handles signing automatically. API keys avoid per-request signing entirely and enable drop-in compatibility with standard Solana libraries via ?api-key= in the URL.
Recommendation for bots: wallet signatures are fully supported for keyless autonomous operation and must remain available. Use API keys when the bot operator is comfortable managing a generated secret and wants the absolute lowest auth overhead.
Endpoint Profile
The default Frankfurt endpoint is a hot-node profile optimized for:
- Recent state reads: getSlot, getLatestBlockhash, getBalance, getAccountInfo, getMultipleAccounts
- Account/token owner reads and priority-fee helpers
- sendTransaction and sendSmartTransaction
- WebSocket subscriptions
- Yellowstone lite streaming
Archive-only methods such as getEnrichedTransaction, explainTransaction, and getConfirmedTransaction are enabled on dedicated endpoint profiles. When the archive profile is disabled, the request is rejected before billing with a deterministic service-profile error (-32004). Bots should treat -32004 with HTTP 200 as "route to an archive endpoint or skip this workflow." getTransaction and getSignaturesForAddress are served by the hot node directly; getProgramAccounts is not available.
Hot-Path Optimization Tips
-
Choose auth mode deliberately. Wallet signatures are the zero-setup default โ the SDK signs every request automatically. API keys avoid per-request signing and are the lowest-latency option. If you are latency-sensitive, generate an API key early. Session tokens are useful for long-lived dApp or WebSocket clients.
-
Hold persistent connections. A cold TLS handshake adds 2-5s to the first request. Reuse HTTP connections via
requests.Session()(Python), a sharedAgent(Node.js), or a connection pool. Nodius keeps server-side connections alive with a 30s idle timeout โ a request every 30s keeps a connection warm. For real-time data, a single WebSocket subscription avoids per-request HTTP overhead entirely. -
Batch requests when possible. A batch of 10 getBalance calls = 1 HTTP round-trip, billed as 10 credits. Rate limit counts as 1 request.
-
Use getMultipleAccounts instead of N ร getAccountInfo. 1 credit per account, single round-trip. Max 100 accounts per call.
-
Cache getLatestBlockhash locally. Blockhash is valid for ~60 seconds. Cache client-side, refresh every 30s.
-
Prefer confirmed over finalized commitment. Finalized adds ~13 seconds of latency. Use confirmed for reads, finalized only when required.
-
Monitor X-Credits-Remaining header. Included in every response. Automate reloads when credits drop below threshold. Unused deposit credits expire after 10 days.
-
Use bulk endpoints for multi-account queries. POST /bulk/getBalances (up to 100 pubkeys), POST /bulk/getTokenBalances (up to 50 owners), POST /bulk/getTransactions (up to 50 signatures).
-
For streaming data, use WebSocket or Yellowstone gRPC. WS: 5 credits per subscription + 1 per notification. Yellowstone: 60 credits/minute (flat rate, unlimited notifications). Better than polling for real-time account changes.
Machine-Readable Error Codes
| HTTP | RPC Code | Constant | Credits? | Retry? | SDK error class |
|---|---|---|---|---|---|
| 402 | -32010 | INSUFFICIENT_CREDITS |
No | After deposit | InsufficientCreditsError |
| 402 | -32002 | PAYMENT_REQUIRED |
No | After deposit | InsufficientCreditsError |
| 429 | -32005 | RATE_LIMITED |
No | Yes (Retry-After) |
RateLimitError |
| 429 | -32005 | CONCURRENCY_LIMITED |
No | Yes (1s + jitter) | RateLimitError |
| 502 | -32003 | BACKEND_UNAVAILABLE |
No | Yes (backoff) | BackendUnavailableError |
| 503 | -32003 | SERVICE_TEMPORARILY_UNAVAILABLE |
No | Yes (backoff) | BackendUnavailableError |
| 403 | -32001 | METHOD_NOT_ALLOWED |
No | No | MethodNotAllowedError |
| 200 | -32004 | SERVICE_PROFILE_DISABLED |
No | No | ServiceProfileDisabledError |
| 403 | โ | NONCE_REUSED |
No | No (new nonce) | AuthenticationError |
| 401 | -32000 | AUTH_FAILED |
No | No (fix creds) | AuthenticationError / ExpiredSessionError |
| 400 | -32600 | INVALID_REQUEST |
No | No (fix request) | NodiusError |
| 200 | -32601 | METHOD_NOT_FOUND |
No | No | MethodNotAllowedError / RpcError |
Credits are never consumed on any error response โ billing happens only after the upstream node returns a successful result. For the full error reference including upstream Solana node codes and disambiguation guidance, see Error Reference.
Response headers on every request: - X-Credits-Remaining: current credit balance - X-Credits-Low: true (when balance < 1000) - Retry-After: seconds (on 429)
Error Handling Best Practice
Use SDK error classes for production bots. This keeps wallet-only auth intact and avoids hand-written signing bugs.
import {
BackendUnavailableError,
InsufficientCreditsError,
NodiusClient,
RateLimitError,
ServiceProfileDisabledError,
} from "@nodius/sdk";
async function rpcCall(rpc: NodiusClient, method: string, params: unknown[] = []) {
try {
return await rpc.call(method, params);
} catch (err) {
if (err instanceof RateLimitError) {
await new Promise((resolve) => setTimeout(resolve, err.retryAfter * 1000));
return rpcCall(rpc, method, params);
}
if (err instanceof InsufficientCreditsError) {
throw new Error("Out of credits - deposit USDC from the registered wallet");
}
if (err instanceof ServiceProfileDisabledError) {
throw new Error(`Route ${method} to another endpoint profile or skip it`);
}
if (err instanceof BackendUnavailableError) {
throw new Error("Backend unavailable - retry with jitter or fail over");
}
throw err;
}
}