Authentication

Nodius uses your Solana wallet for authentication. Wallet-native signup. API keys and sessions available for low-latency automated traffic. Your Ed25519 keypair is your identity.

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, scripts Generate a persistent API key

Mode 1: Per-Request Wallet Signature (v2)

Every request is independently signed with your Solana keypair. This is the canonical authentication mode and the most secure option for automated systems โ€” there's no token to steal or expire.

Note: Auth v1 is removed. Only v2 is supported. The server verifies using the actual HTTP method and path from the request (server-side), not client-supplied headers. There is no X-Auth-Version, X-Http-Method, or X-Http-Path header.

How It Works

  1. Compute the SHA-256 hex digest of the request body
  2. Construct the canonical signing message (see below)
  3. Sign the message with your Ed25519 private key
  4. 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 (server uses actual request path)
TIMESTAMP Unix timestamp in seconds (must be within ยฑ60s of server time)
NONCE Random string, 1-128 chars, alphanumeric plus -_:., (prevents replay attacks)
BODY_HASH SHA-256 hash of the request body (hex-encoded, lowercase)

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 (1-128 chars)

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. The server maintains a nonce cache using Redis SETNX with a TTL. Reusing a nonce results in a -32000 error (replay detected).

The timestamp must be within ยฑ60 seconds of the server's clock. Requests outside this window are rejected.

Code Example (TypeScript)

This low-level example uses @solana/web3.js, tweetnacl, and bs58 directly. The SDK handles this signing path for you when you pass any compatible KeypairLike.

import { Keypair } from "@solana/web3.js";
import { sign } from "tweetnacl";
import { createHash, randomBytes } from "crypto";
import bs58 from "bs58";

function signRequest(keypair: Keypair, method: string, path: string, body: string) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = randomBytes(16).toString("hex");
  const bodyHash = createHash("sha256").update(body).digest("hex");

  // Canonical v2 signing message
  const message = `solana-nodius:v2:${method}:${path}:${timestamp}:${nonce}:${bodyHash}`;
  const messageBytes = new TextEncoder().encode(message);
  const signature = sign.detached(messageBytes, keypair.secretKey);

  return {
    "X-Pubkey": keypair.publicKey.toBase58(),
    "X-Signature": bs58.encode(Buffer.from(signature)),
    "X-Timestamp": timestamp,
    "X-Nonce": nonce,
  };
}

// Usage
const keypair = Keypair.generate();
const body = JSON.stringify({
  jsonrpc: "2.0",
  id: 1,
  method: "getSlot",
});

const headers = signRequest(keypair, "POST", "/", body);

const response = await fetch("https://rpc.nodius.xyz/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    ...headers,
  },
  body,
});

console.log("Credits remaining:", response.headers.get("X-Credits-Remaining"));
const result = await response.json();
console.log("Slot:", result.result);

Code Example (Python)

import time
import os
import hashlib
import json
import requests
import base58
from solders.keypair import Keypair
from nacl.signing import SigningKey

keypair = Keypair()

body = json.dumps({
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getSlot"
})

timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()
body_hash = hashlib.sha256(body.encode()).hexdigest()

# Canonical v2 signing message
message = f"solana-nodius:v2:POST:/:{ timestamp}:{nonce}:{body_hash}"

signing_key = SigningKey(bytes(keypair)[:32])
signature = signing_key.sign(message.encode()).signature

response = requests.post(
    "https://rpc.nodius.xyz/",
    headers={
        "Content-Type": "application/json",
        "X-Pubkey": str(keypair.pubkey()),
        "X-Signature": base58.b58encode(signature).decode("ascii"),
        "X-Timestamp": timestamp,
        "X-Nonce": nonce,
    },
    data=body,
)

print("Credits remaining:", response.headers.get("X-Credits-Remaining"))
print("Result:", response.json())

Mode 2: Session Tokens

Session tokens are useful when per-request signing is impractical โ€” WebSocket connections, browser dApps, or situations where you want to authenticate once and reuse a bearer token.

How It Works

  1. Request a challenge from the server
  2. Sign the challenge with your keypair
  3. Submit the signed challenge to receive a session token
  4. Use the token as a Bearer token in subsequent requests

Step 1: Request a Challenge

POST /auth/challenge
Content-Type: application/json

{
  "pubkey": "YourPublicKeyBase58..."
}

Response:

{
  "challenge": "a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890",
  "expires_in": 300
}

The challenge is a 64-character hex string (32 random bytes, hex-encoded). It is valid for 300 seconds (5 minutes).

Step 2: Sign and Verify

Sign the raw challenge hex string with your Ed25519 keypair, then submit:

POST /auth/verify
Content-Type: application/json

{
  "pubkey": "YourPublicKeyBase58...",
  "challenge": "a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890",
  "signature": "Base58EncodedSignature..."
}

Response:

{
  "token": "64-char-hex-session-token...",
  "expires_in": 3600
}

The token is a 64-character hex string (256-bit random, hex-encoded). It expires after 3600 seconds (1 hour) with a sliding TTL that refreshes on each use.

Step 3: Use the Token

Include the token in the Authorization header:

POST /
Authorization: Bearer <token>
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"getSlot"}

This also works at POST /rpc.

For WebSocket connections, use the Authorization header or Sec-WebSocket-Protocol header:

Authorization: Bearer <token>

Or via the Sec-WebSocket-Protocol header (for clients that don't support custom headers):

Sec-WebSocket-Protocol: auth.<token>, solana-rpc

Both auth.<token> and solana-rpc subprotocols must be sent together. The server responds with Sec-WebSocket-Protocol: solana-rpc (the token is never echoed back).

Revoking Sessions

To revoke the current session:

POST /auth/logout
Authorization: Bearer <token>

Session Details

Code Example

import { Keypair } from "@solana/web3.js";
import { sign } from "tweetnacl";
import bs58 from "bs58";

async function getSessionToken(
  endpoint: string,
  keypair: Keypair
): Promise<string> {
  // Step 1: Get challenge
  const challengeRes = await fetch(`${endpoint}/auth/challenge`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      pubkey: keypair.publicKey.toBase58(),
    }),
  });
  const { challenge } = await challengeRes.json();

  // Step 2: Sign the raw challenge hex string and verify
  const challengeBytes = new TextEncoder().encode(challenge);
  const signature = sign.detached(challengeBytes, keypair.secretKey);

  const verifyRes = await fetch(`${endpoint}/auth/verify`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      pubkey: keypair.publicKey.toBase58(),
      challenge,
      signature: bs58.encode(Buffer.from(signature)),
    }),
  });
  const { token } = await verifyRes.json();

  return token;
}

// Usage
const keypair = Keypair.generate();
const token = await getSessionToken("https://rpc.nodius.xyz", keypair);

// Now use the token for requests
const res = await fetch("https://rpc.nodius.xyz/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
  },
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getSlot" }),
});

Mode 3: API Key

For simple integrations, you can generate a persistent API key. This avoids per-request signing while providing a long-lived credential.

Generate an API Key

POST /account/api-key

Requires per-request wallet signature auth (or an existing session token).

Response:

{
  "ok": true,
  "api_key": "srpc_live_<base64url>...",
  "message": "Save this key โ€” it will not be shown again."
}

Use the API Key

Send it via the X-Api-Key header:

POST /
X-Api-Key: <api-key>
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"getSlot"}

API keys do not expire but can be regenerated (which invalidates the previous key).

Two API key formats are accepted: - New format: srpc_live_<base64url> (recommended) - Legacy format: 64-character hex string

API Key for WebSocket

API keys also work for WebSocket connections via Sec-WebSocket-Protocol:

Sec-WebSocket-Protocol: auth.<api-key>, solana-rpc

Or via the X-Api-Key header (for clients that support custom WebSocket headers).

Auth Priority

When multiple auth headers are present, the server checks in this order:

  1. X-Api-Key โ€” if present, API key auth is used (fastest)
  2. X-Pubkey โ€” if present, per-request wallet signature auth is used
  3. Authorization: Bearer โ€” if present, session token auth is used

If none are present, the request is rejected with a -32000 error.

Which Mode Should I Use?

Use per-request signatures if: - You're running a bot or automated system - You have access to the keypair at request time - You want maximum security (no token to leak) - You're making HTTP requests (not WebSocket)

Use session tokens if: - You need WebSocket connections - You're building a dApp or UI - You want to authenticate once and make many requests - You're using a language/environment where per-request signing is cumbersome

Use API keys if: - You want the simplest possible integration - You're testing or prototyping - You're comfortable storing a long-lived secret - You need both HTTP and WebSocket with one credential

SDK Authentication

The @nodius/sdk package handles all authentication automatically:

const rpc = new NodiusClient("https://rpc.nodius.xyz", {
  keypair,
  // Default: per-request signing (v2)
  // auth: "signature",

  // Or use session tokens (auto-renews before expiry)
  // auth: "session",
});

// Just make calls โ€” auth is handled
const slot = await rpc.call("getSlot");