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.
Recommended Onboarding Flow
- Generate a Solana keypair (Ed25519)
- POST /account/activate with wallet signature headers
- Returns: pubkey, optional api_key, deposit_address, funding instructions, balance
- Send USDC to the deposit_address from the same wallet
- Wait ~30s for deposit confirmation
- Start making RPC calls using either wallet signatures or the optional API key
Minimal wallet-only TypeScript flow:
import { Keypair } from "@solana/web3.js";
import { NodiusClient } from "@nodius/sdk";
const keypair = Keypair.generate();
const rpc = new NodiusClient("https://rpc.nodius.xyz", { keypair });
const account = await rpc.activate();
console.log("Deposit USDC from this wallet to:", account.deposit_address);
console.log("Credit rate:", account.credits_per_usdc, "credits / USDC");
console.log("Credit expiry:", account.credit_expiry_seconds, "seconds");
const info = await rpc.getAccountInfo();
if (info.balance <= 0) throw new Error("Deposit USDC before sending traffic");
const slot = await rpc.getSlot();
console.log(slot);
API keys remain optional. Generate one only when the bot can safely manage a generated secret:
const { api_key } = await rpc.generateApiKey();
Auth Methods Comparison
| Method | Latency | Security | Best For |
|---|---|---|---|
| API Key (X-Api-Key) | Lowest (~0ms overhead) | Medium (secret storage) | Bots, scripts, high-frequency trading |
| Per-request wallet sig | Low (~1ms overhead) | Highest (no API key secret) | Security-critical automated systems |
| Session token (Bearer) | Low (~0.5ms lookup) | Medium (1h expiry) | WebSocket connections, dApps |
Recommendation for bots: wallet signatures are fully supported for keyless autonomous operation and must remain available. Use API keys only 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/history methods such as getTransaction, getSignaturesForAddress, and getBlock, plus heavy indexed scans such as getProgramAccounts, are enabled on dedicated endpoint profiles. When a profile is disabled, the request is rejected before billing with a deterministic service-profile error. Bots should treat -32004 as "route to an archive/heavy endpoint or skip this workflow."
Hot-Path Optimization Tips
- Choose auth mode deliberately
- Wallet signatures avoid API-key storage and are the preferred keyless bot mode
- API key auth is lowest latency when a bot can safely manage a generated secret
-
Session tokens are useful for long-lived dApp or WebSocket clients
-
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 x 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 100 pubkeys)
-
POST /bulk/getTransactions (up to 20 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 Status | JSON-RPC Code | Constant | Description |
|---|---|---|---|
| 402 | -32010 | INSUFFICIENT_CREDITS | Balance too low for this request |
| 429 | -32005 | RATE_LIMITED | Per-second rate limit exceeded |
| 429 | -32005 | CONCURRENCY_LIMITED | Too many concurrent requests |
| 502 | -32003 | BACKEND_UNAVAILABLE | Upstream Solana node unreachable |
| 403 | -32001 | METHOD_NOT_ALLOWED | Method denied |
| 200 | -32004 | SERVICE_PROFILE_DISABLED | Archive/history or heavy-indexed profile is not enabled; no credits consumed |
| 503 | -32003 | SERVICE_TEMPORARILY_UNAVAILABLE | Transient service gate/dependency condition; no credits consumed when rejected before billing |
| 401 | -32000 | AUTH_FAILED | Invalid signature, expired token, or bad API key |
| 400 | -32600 | INVALID_REQUEST | Malformed JSON-RPC request |
| 200 | -32601 | METHOD_NOT_FOUND | Unknown RPC method |
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;
}
}