Authentication
Nodius uses your Solana wallet for authentication. Your Ed25519 keypair is your identity. No email signup required.
Identity, Not Accounts
Your Solana public key is your identity on Nodius. Credits are tied to your pubkey โ lose your keypair, lose your credits. There is no separate "account" you create or log into. What we call an "account" internally is just a database row keyed by your pubkey that tracks your credit balance and API key.
Three things that are often conflated:
| What | Requires | When it works |
|---|---|---|
| Authentication | A valid wallet signature | Always โ regardless of balance or account record |
Account-info endpoints (GET /account/info, POST /account/api-key) |
An account record | After first deposit, or after calling POST /account/api-key (creates it with zero balance) |
Billable RPC calls (POST /, /rpc) |
Positive credit balance | After a USDC deposit is confirmed (~30 seconds) |
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.
The Lifecycle in Practice
- Generate or load a Solana keypair โ your pubkey is your identity
- Buy credits โ send USDC (SPL) on Solana mainnet to the deposit address from that wallet (minimum $0.01); the account record is created automatically and credits appear within seconds
- Make RPC calls โ the SDK signs every request; credits are deducted per call
- (Optional) Generate an API key via
POST /account/api-keyfor lower-latency auth. This also creates the account record if it doesn't exist yet โ soGET /account/infoworks before any deposit.
With x402 auto-pay enabled in the SDK, step 2 happens automatically โ the first
402triggers a micro-deposit, credits land, and the call is retried transparently.
Three authentication modes are available:
| Mode | Best for | How it works |
|---|---|---|
| Per-request signature (v2) | Bots, scripts, automated systems | Sign every request with your keypair |
| Session token | WebSocket, dApps, interactive use | Authenticate once, get a bearer token |
| API key | Simple integrations, curl, scripts | Generate a persistent API key |
Per-Request Wallet Signature (v2)
Every request is independently signed with your Solana keypair. This is the canonical authentication mode โ the most secure option for automated systems. There's no token to steal or expire.
How It Works
- Compute the SHA-256 hex digest of the request body
- Construct the canonical signing message (see below)
- Sign the message with your Ed25519 private key
- Include the signature and metadata in request headers
Canonical Signing Message
solana-nodius:v2:{METHOD}:{PATH}:{TIMESTAMP}:{NONCE}:{BODY_HASH}
| Component | Description |
|---|---|
METHOD |
HTTP method, e.g. POST (server uses actual request method) |
PATH |
Request path, e.g. / or /rpc or /account/info |
TIMESTAMP |
Unix timestamp in seconds (must be within ยฑ60s of server time) |
NONCE |
Random string, 1-128 chars, alphanumeric plus -_:., |
BODY_HASH |
SHA-256 hex digest of the request body (empty string for GET) |
Required Headers
| Header | Value |
|---|---|
X-Pubkey |
Your Solana public key (base58, 32 bytes) |
X-Signature |
Ed25519 signature of the signing message (base58, 64 bytes) |
X-Timestamp |
Unix timestamp used in the signing message |
X-Nonce |
Nonce used in the signing message |
No X-Auth-Version, X-Http-Method, or X-Http-Path headers are needed โ the server infers the method and path from the actual request.
Replay Protection
Each (pubkey, nonce) pair can only be used once. Reusing a nonce results in an HTTP 403 error with a string error code NONCE_REUSED. The timestamp must be within ยฑ60 seconds of the server's clock.
Code Examples
import { NodiusClient } from "@nodius/sdk";
const rpc = NodiusClient.fromSecretKey(
"https://rpc.nodius.xyz",
process.env.SECRET_KEY_BASE58!
);
// The SDK signs every request automatically โ just make calls:
const slot = await rpc.call("getSlot");
console.log("Slot:", slot);
from nodius import NodiusClient
rpc = NodiusClient.from_base58(
"https://rpc.nodius.xyz",
secret_key="your_base58_secret_key_here",
)
# The SDK signs every request automatically โ just make calls:
slot = rpc.call("getSlot")
print("Slot:", slot)
import time, os, hashlib, json, urllib.request
import base58
from nacl.signing import SigningKey
SECRET_KEY_BASE58 = "your_base58_secret_key_here"
BASE_URL = "https://rpc.nodius.xyz"
# Decode keypair (32-byte seed or 64-byte expanded key)
raw = base58.b58decode(SECRET_KEY_BASE58)
if len(raw) == 32:
signing_key = SigningKey(raw)
pubkey_bytes = bytes(signing_key.verify_key)
elif len(raw) == 64:
signing_key = SigningKey(raw[:32])
pubkey_bytes = raw[32:]
else:
raise ValueError(f"Secret key must be 32 or 64 bytes, got {len(raw)}")
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "getSlot"})
body_bytes = body.encode()
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()
body_hash = hashlib.sha256(body_bytes).hexdigest()
message = f"solana-nodius:v2:POST:/:{timestamp}:{nonce}:{body_hash}"
signature = signing_key.sign(message.encode()).signature
req = urllib.request.Request(
BASE_URL, data=body_bytes,
headers={
"Content-Type": "application/json",
"X-Pubkey": base58.b58encode(pubkey_bytes).decode(),
"X-Signature": base58.b58encode(signature).decode(),
"X-Timestamp": timestamp,
"X-Nonce": nonce,
},
)
with urllib.request.urlopen(req) as resp:
print("Credits remaining:", resp.headers.get("X-Credits-Remaining"))
print("Result:", json.loads(resp.read()))
const crypto = require("crypto");
const nacl = require("tweetnacl");
const bs58 = require("bs58");
const SECRET_KEY_BASE58 = "your_base58_secret_key_here";
const raw = bs58.decode(SECRET_KEY_BASE58);
let secretKey, publicKeyBytes;
if (raw.length === 32) {
const kp = nacl.sign.keyPair.fromSeed(raw);
secretKey = kp.secretKey;
publicKeyBytes = kp.publicKey;
} else if (raw.length === 64) {
secretKey = raw;
publicKeyBytes = raw.slice(32);
}
const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getSlot" });
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const message = `solana-nodius:v2:POST:/:${timestamp}:${nonce}:${bodyHash}`;
const signature = nacl.sign.detached(new TextEncoder().encode(message), secretKey);
const res = await fetch("https://rpc.nodius.xyz", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Pubkey": bs58.encode(publicKeyBytes),
"X-Signature": bs58.encode(signature),
"X-Timestamp": timestamp,
"X-Nonce": nonce,
},
body,
});
const result = await res.json();
console.log("Result:", result);
# curl cannot sign requests directly. Use an API key instead:
curl -X POST https://rpc.nodius.xyz \
-H "Content-Type: application/json" \
-H "X-Api-Key: srpc_live_..." \
-d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}'
# Or use one of the raw Python/JS examples to sign requests.
Session Tokens
Authenticate once with your keypair, get a bearer token, then use it for subsequent requests. Useful for WebSocket connections, dApps, or when per-request signing is impractical.
Flow
- Request a challenge from
POST /auth/challengewith your pubkey - Sign the challenge โ sign the 64-character hex string as UTF-8 bytes with your Ed25519 keypair (do not hex-decode it into 32 bytes)
- Submit the signed challenge to
POST /auth/verifyto receive a session token - Use the token as
Authorization: Bearer <token>
Session Details
- Duration: 1 hour (sliding TTL refreshed on each use)
- Absolute max: 24 hours regardless of activity
- Concurrency: Up to 10 active sessions per account
- Revocation:
POST /auth/logout
const rpc = NodiusClient.fromSecretKey(
"https://rpc.nodius.xyz",
process.env.SECRET_KEY_BASE58!
);
// SDK handles challenge + sign + verify in one call:
const token = await rpc.authenticate();
console.log("Session token:", token);
// Now use the token for requests or WebSocket connections
rpc = NodiusClient.from_base58(
"https://rpc.nodius.xyz",
secret_key="your_base58_secret_key_here",
)
# SDK handles challenge + sign + verify in one call:
token = rpc.authenticate()
print("Session token:", token)
import json, urllib.request
import base58
from nacl.signing import SigningKey
BASE_URL = "https://rpc.nodius.xyz"
SECRET_KEY_BASE58 = "your_base58_secret_key_here"
raw = base58.b58decode(SECRET_KEY_BASE58)
signing_key = SigningKey(raw[:32]) if len(raw) == 64 else SigningKey(raw)
pubkey_b58 = base58.b58encode(
raw[32:] if len(raw) == 64 else bytes(signing_key.verify_key)
).decode()
# Step 1: Get challenge
req1 = urllib.request.Request(
f"{BASE_URL}/auth/challenge",
data=json.dumps({"pubkey": pubkey_b58}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req1) as resp:
challenge = json.loads(resp.read())["challenge"]
# Step 2: Sign the challenge hex string (as UTF-8 bytes)
signature = signing_key.sign(challenge.encode()).signature
sig_b58 = base58.b58encode(signature).decode()
# Step 3: Verify and get token
req2 = urllib.request.Request(
f"{BASE_URL}/auth/verify",
data=json.dumps({
"pubkey": pubkey_b58,
"challenge": challenge,
"signature": sig_b58,
}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req2) as resp:
token = json.loads(resp.read())["token"]
print("Session token:", token)
const nacl = require("tweetnacl");
const bs58 = require("bs58");
const SECRET_KEY_BASE58 = "your_base58_secret_key_here";
const raw = bs58.decode(SECRET_KEY_BASE58);
let secretKey, publicKeyBytes;
if (raw.length === 32) {
const kp = nacl.sign.keyPair.fromSeed(raw);
secretKey = kp.secretKey;
publicKeyBytes = kp.publicKey;
} else {
secretKey = raw;
publicKeyBytes = raw.slice(32);
}
const pubkeyB58 = bs58.encode(publicKeyBytes);
// Step 1: Get challenge
const res1 = await fetch("https://rpc.nodius.xyz/auth/challenge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ pubkey: pubkeyB58 }),
});
const { challenge } = await res1.json();
// Step 2: Sign the challenge hex string (UTF-8 encode the 64-char hex text, do NOT hex-decode)
const sigBytes = nacl.sign.detached(new TextEncoder().encode(challenge), secretKey);
const sigB58 = bs58.encode(sigBytes);
// Step 3: Verify and get token
const res2 = await fetch("https://rpc.nodius.xyz/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ pubkey: pubkeyB58, challenge, signature: sigB58 }),
});
const { token } = await res2.json();
console.log("Session token:", token);
# Step 1: Get challenge
curl -X POST https://rpc.nodius.xyz/auth/challenge \
-H "Content-Type: application/json" \
-d '{"pubkey":"YourPublicKeyBase58..."}'
# Step 2: Sign the challenge with your Ed25519 keypair
# (use a Python or JS script for the signing step)
# Step 3: Verify and get token
curl -X POST https://rpc.nodius.xyz/auth/verify \
-H "Content-Type: application/json" \
-d '{"pubkey":"...","challenge":"...","signature":"..."}'
# Step 4: Use the token
curl -X POST https://rpc.nodius.xyz \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_token_here" \
-d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}'
API Keys
For simple integrations, generate a persistent API key. This avoids per-request signing while providing a long-lived credential.
Generate an API Key
Call POST /account/api-key with wallet signature auth. If no account record exists yet, it is created automatically with a zero balance โ so you can get an API key and check your deposit address before buying credits.
const rpc = NodiusClient.fromSecretKey(
"https://rpc.nodius.xyz",
process.env.SECRET_KEY_BASE58!
);
const { api_key } = await rpc.generateApiKey();
console.log("API key:", api_key);
// The SDK auto-stores the API key for subsequent calls
rpc = NodiusClient.from_base58(
"https://rpc.nodius.xyz",
secret_key="your_base58_secret_key_here",
)
result = rpc.generate_api_key()
print("API key:", result["api_key"])
# The SDK auto-stores the API key for subsequent calls
# This endpoint requires wallet signature auth.
# See the "Per-Request Wallet Signature" section above for
# the full signing algorithm. The only difference is:
# PATH = "/account/api-key"
# body = "{}"
// This endpoint requires wallet signature auth.
// See the "Per-Request Wallet Signature" section above for
// the full signing algorithm. The only difference is:
// PATH = "/account/api-key"
// body = "{}"
# This endpoint requires wallet signature auth โ curl cannot sign
# requests directly. Use an SDK or a raw Python/JS script.
Use the API Key
Two ways to pass the API key โ header or URL query parameter:
curl -X POST https://rpc.nodius.xyz \
-H "Content-Type: application/json" \
-H "X-Api-Key: srpc_live_..." \
-d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}'
curl -X POST "https://rpc.nodius.xyz/?api-key=srpc_live_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}'
import { Connection } from "@solana/web3.js";
// Paste one URL โ every standard method just works
const conn = new Connection("https://rpc.nodius.xyz/?api-key=srpc_live_...");
const slot = await conn.getSlot();
from solana.rpc.api import Client
# Paste one URL โ every standard method just works
client = Client("https://rpc.nodius.xyz/?api-key=srpc_live_...")
slot = client.get_slot()
The URL parameter enables drop-in compatibility with standard Solana libraries โ no custom client wrapper needed. The header is checked first; if absent, the query parameter is used.
API keys do not expire but can be regenerated (which invalidates the previous key). Two formats are accepted: srpc_live_<base64url> (recommended) and legacy 64-character hex strings.
API Key for WebSocket
API keys also work for WebSocket connections via Sec-WebSocket-Protocol:
Sec-WebSocket-Protocol: auth.<api-key>, solana-rpc
Auth Priority
When multiple auth methods are present, the server checks in this order:
X-Api-Keyheader or?api-key=URL parameter โ API key auth (lowest latency)X-Pubkeyโ per-request wallet signature authAuthorization: Bearer ***โ session token auth
If none are present, the request is rejected with an HTTP 401 error (AUTH_FAILED).
Which Mode Should I Use?
All three modes work for every request type โ HTTP, WebSocket, and gRPC. The difference is latency and convenience.
Wallet signatures are the zero-setup default. The SDK signs every request automatically. Each request involves an Ed25519 signature, SHA-256 body hash, and a Redis nonce check for replay protection. For most workloads this overhead is negligible, but if you are latency-sensitive you will get lower latency with an API key.
API keys avoid per-request signing entirely โ a single header or URL parameter replaces the signature, nonce, and timestamp headers. This is the lowest-latency auth mode and enables drop-in compatibility with standard Solana libraries via ?api-key= in the URL.
Session tokens are useful for WebSocket connections or when per-request signing is impractical. Authenticate once with a wallet signature, get a 1-hour bearer token.
SDK Authentication
The @nodius/sdk and nodius Python packages handle all authentication automatically. generateApiKey() / generate_api_key() generates an API key, creates the account record if needed, and stores the key for subsequent calls. Wallet-signature auth works without it โ but billable RPC calls require credits (deposit USDC first, or enable x402 auto-pay).
import { NodiusClient } from "@nodius/sdk";
// From base58 secret key (recommended โ no @solana/web3.js needed)
const rpc = NodiusClient.fromSecretKey(
"https://rpc.nodius.xyz",
process.env.SECRET_KEY_BASE58!
);
// Optional: generate API key
await rpc.generateApiKey();
// Just make calls โ auth is handled
const slot = await rpc.call("getSlot");
from nodius import NodiusClient
# From base58 secret key (recommended)
rpc = NodiusClient.from_base58(
"https://rpc.nodius.xyz",
secret_key="your_base58_secret_key_here",
)
# Optional: generate API key
rpc.generate_api_key()
# Just make calls โ auth is handled
slot = rpc.call("getSlot")
# Use API key auth after generating one:
import json, urllib.request
API_KEY = "srpc_live_your_api_key_here"
req = urllib.request.Request(
"https://rpc.nodius.xyz",
data=json.dumps({"jsonrpc": "2.0", "id": 1, "method": "getSlot"}).encode(),
headers={
"Content-Type": "application/json",
"X-Api-Key": API_KEY,
},
method="POST",
)
with urllib.request.urlopen(req) as resp:
print(json.loads(resp.read()))
const API_KEY = "srpc_live_your_api_key_here";
const res = await fetch("https://rpc.nodius.xyz", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": API_KEY,
},
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getSlot" }),
});
const result = await res.json();
console.log(result);
curl -X POST https://rpc.nodius.xyz \
-H "Content-Type: application/json" \
-H "X-Api-Key: srpc_live_..." \
-d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}'