Fix Hermes and OpenClaw 403 with a local proxy
Some clients may return 403 when they call the official 678API endpoint directly because of client networking, headers, certificates, or proxy-chain differences. Run a lightweight local Node.js proxy and point the client to localhost instead.
If Cursor, Chatbox, or other clients can use 678API normally, but Hermes or OpenClaw returns 403 after you set https://www.678api.com/v1 directly, use this guide. This proxy does not bypass account permissions, credits, or model access. It only forwards requests from your local client to 678API.
Final client settings
http://127.0.0.1:8787/v1local-proxyhttps://www.wintoken.dev/v1https://www.678api.com/v1Stored in the local proxy env fileWindows 10 / 11 (recommended: one-click PowerShell setup)
Install Node.js 18 or later, then reopen PowerShell. A standard user account is enough; administrator mode is not required.
Press the Windows key, search for “PowerShell,” and open it. Do not run these commands in Command Prompt.
Run the following three lines. The installer asks for your real 678API API key. No characters appear while you type the key; this is intentional.
$installer = "$env:TEMP\api678-proxy-windows.ps1"
Invoke-WebRequest "https://doc.wintoken.dev/assets/api678-proxy-windows.ps1" -OutFile $installer
powershell -NoProfile -ExecutionPolicy Bypass -File $installerDownload the Windows installer from wintoken.dev
The installer and proxy core are downloaded from doc.wintoken.dev, and the proxy connects to https://www.wintoken.dev/v1 by default instead of depending on the blocked 678api.com domain. It installs under %LOCALAPPDATA%\API678\Proxy, listens only on 127.0.0.1:8787, and adds a startup entry for the current Windows user.
If the current network can access 678api.com, you can explicitly select the original endpoint during installation:
powershell -NoProfile -ExecutionPolicy Bypass -File $installer -UpstreamBaseUrl "https://www.678api.com/v1"Verify the Windows installation
After installation, run these commands in PowerShell:
Invoke-RestMethod "http://127.0.0.1:8787/health"
Invoke-RestMethod "http://127.0.0.1:8787/v1/models" -Headers @{ Authorization = "Bearer local-proxy" }The first command should return ok: true; the second should return model data. If Windows Firewall prompts for Node.js access, public-network access is unnecessary because the proxy only uses the local loopback address.
Configure Hermes / OpenClaw on Windows
Create an OpenAI Compatible / Custom OpenAI Endpoint in Hermes or OpenClaw provider settings.
Use http://127.0.0.1:8787/v1. Do not append /chat/completions.
Use local-proxy. Do not enter your real 678API key in the client again.
Quit and reopen Hermes. For OpenClaw, restart the Gateway or reopen the client so it does not keep the old endpoint in cache.
Windows maintenance
Follow the live log:
Get-Content "$env:LOCALAPPDATA\API678\Proxy\api678-proxy.log" -WaitChange the real API key:
notepad "$env:LOCALAPPDATA\API678\Proxy\api678-proxy.env"After saving, rerun the installer to restart the proxy safely. It only stops the proxy process recorded in its own PID file and does not stop unrelated Node.js applications. To inspect the port:
Get-NetTCPConnection -LocalPort 8787 -ErrorAction SilentlyContinueIf “node is not recognized” appears, install Node.js 18+ and reopen PowerShell. If script execution is blocked, use the -ExecutionPolicy Bypass command above. If the health check fails, inspect api678-proxy.err.log and confirm that port 8787 is available.
macOS / Linux manual setup
The original macOS / Linux guide continues below. Windows users who completed the installer and client setup above do not need these steps.
Create a folder
mkdir -p ~/api678-proxy
cd ~/api678-proxyCreate the proxy script
Create api678-proxy.mjs:
#!/usr/bin/env node
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { URL, fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
loadEnvFile(path.join(scriptDir, "api678-proxy.env"));
const listenHost = process.env.LISTEN_HOST || "127.0.0.1";
const listenPort = Number(process.env.PORT || "8787");
const upstreamBase = process.env.UPSTREAM_BASE_URL || "https://www.678api.com/v1";
const upstreamApiKey = process.env.UPSTREAM_API_KEY || "";
const localPrefix = normalizePrefix(process.env.LOCAL_PREFIX || "/v1");
const upstreamTimeoutMs = Number(process.env.UPSTREAM_TIMEOUT_MS || "300000");
if (!upstreamBase) {
console.error("Missing UPSTREAM_BASE_URL");
process.exit(1);
}
const baseUrl = new URL(upstreamBase);
baseUrl.pathname = stripTrailingSlash(baseUrl.pathname);
const server = http.createServer(async (req, res) => {
req.socket?.setNoDelay(true);
res.socket?.setNoDelay(true);
if (req.method === "OPTIONS") {
writeCors(res);
res.writeHead(204);
res.end();
return;
}
if (req.url === "/health" || req.url === "/") {
writeCors(res);
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ ok: true, upstream: redactUrl(baseUrl) }));
return;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
try {
const target = buildTargetUrl(req.url);
const headers = buildHeaders(req.headers);
const body = ["GET", "HEAD"].includes((req.method || "GET").toUpperCase()) ? undefined : req;
const upstreamResp = await fetch(target, {
method: req.method,
headers,
body,
duplex: "half",
redirect: "manual",
signal: controller.signal,
});
writeCors(res);
for (const [key, value] of upstreamResp.headers.entries()) {
if (!isHopByHopHeader(key) && !["content-encoding", "content-length"].includes(key.toLowerCase())) {
res.setHeader(key, value);
}
}
res.writeHead(upstreamResp.status, upstreamResp.statusText);
res.flushHeaders();
if (upstreamResp.body) {
for await (const chunk of upstreamResp.body) res.write(Buffer.from(chunk));
}
res.end();
} catch (error) {
const status = error?.name === "AbortError" ? 504 : 502;
writeCors(res);
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ error: { message: String(error?.message || error), type: "api678_proxy_error" } }));
} finally {
clearTimeout(timeout);
}
});
server.listen(listenPort, listenHost, () => {
console.log("678API proxy listening on http://" + listenHost + ":" + listenPort);
console.log("Local Base URL: http://" + listenHost + ":" + listenPort + localPrefix);
console.log("Forwarding to: " + redactUrl(baseUrl));
});
function buildTargetUrl(requestUrl) {
const incoming = new URL(requestUrl || "/", "http://" + listenHost + ":" + listenPort);
let pathname = incoming.pathname;
if (localPrefix && (pathname === localPrefix || pathname.startsWith(localPrefix + "/"))) {
pathname = pathname.slice(localPrefix.length) || "/";
}
const target = new URL(baseUrl);
target.pathname = joinPath(baseUrl.pathname, pathname);
target.search = incoming.search;
return target;
}
function buildHeaders(incomingHeaders) {
const headers = new Headers();
for (const [key, value] of Object.entries(incomingHeaders)) {
if (!value || isHopByHopHeader(key)) continue;
const lower = key.toLowerCase();
if (["host", "origin", "referer", "content-length"].includes(lower)) continue;
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
}
headers.set("accept", incomingHeaders.accept || "application/json");
headers.set("user-agent", process.env.UPSTREAM_USER_AGENT || "api678-local-proxy/1.0");
if (upstreamApiKey) headers.set("authorization", "Bearer " + upstreamApiKey);
return headers;
}
function writeCors(res) {
res.setHeader("access-control-allow-origin", "*");
res.setHeader("access-control-allow-methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
res.setHeader("access-control-allow-headers", "authorization,content-type,x-requested-with");
}
function isHopByHopHeader(name) {
return ["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"].includes(name.toLowerCase());
}
function normalizePrefix(prefix) {
if (!prefix || prefix === "/") return "";
return "/" + prefix.replace(/^\/+|\/+$/g, "");
}
function joinPath(basePath, requestPath) {
return stripTrailingSlash(basePath) + (requestPath.startsWith("/") ? requestPath : "/" + requestPath);
}
function stripTrailingSlash(pathname) {
if (!pathname || pathname === "/") return "";
return pathname.replace(/\/+$/g, "");
}
function redactUrl(url) {
const safe = new URL(url);
safe.username = "";
safe.password = "";
return safe.toString();
}
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const rawLine of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
const normalizedLine = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
const separatorIndex = normalizedLine.indexOf("=");
if (separatorIndex === -1) continue;
const key = normalizedLine.slice(0, separatorIndex).trim();
let value = normalizedLine.slice(separatorIndex + 1).trim();
if (!key || process.env[key] !== undefined) continue;
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
}Create the env file
Create api678-proxy.env and replace YOUR_API_KEY with your real 678API API key:
LISTEN_HOST=127.0.0.1
PORT=8787
UPSTREAM_BASE_URL=https://www.678api.com/v1
UPSTREAM_API_KEY=YOUR_API_KEY
LOCAL_PREFIX=/v1
UPSTREAM_TIMEOUT_MS=300000
UPSTREAM_USER_AGENT=api678-local-proxy/1.0api678-proxy.env contains your real API key. Do not commit it, screenshot it, or share it publicly.
Start and test
cd ~/api678-proxy
chmod +x api678-proxy.mjs
node api678-proxy.mjsOpen another terminal and test:
curl http://127.0.0.1:8787/health
curl http://127.0.0.1:8787/v1/models -H "Authorization: Bearer local-proxy"If /v1/models returns model data, the local proxy is ready.
Hermes settings
In Hermes model/provider settings, choose OpenAI Compatible or Custom OpenAI Endpoint.
http://127.0.0.1:8787/v1
Use local-proxy. The real key is injected by the local proxy from api678-proxy.env.
Use an available model name from the 678API console. Enable Streaming / Stream for better first-token experience.
OpenClaw settings
Create a custom provider in OpenClaw model settings.
Use http://127.0.0.1:8787/v1. Do not append /chat/completions.
Use local-proxy.
After saving, restart OpenClaw Gateway or reopen the client.
macOS launch at login
To start the proxy automatically after macOS login, create a LaunchAgent:
NODE_BIN="$(command -v node)"
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs"
cat > "$HOME/Library/LaunchAgents/dev.api678.proxy.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>dev.api678.proxy</string>
<key>ProgramArguments</key>
<array>
<string>$NODE_BIN</string>
<string>$HOME/api678-proxy/api678-proxy.mjs</string>
</array>
<key>WorkingDirectory</key>
<string>$HOME/api678-proxy</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/api678-proxy.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/api678-proxy.err.log</string>
</dict>
</plist>
EOF
plutil -lint "$HOME/Library/LaunchAgents/dev.api678.proxy.plist"
launchctl bootout gui/$(id -u) "$HOME/Library/LaunchAgents/dev.api678.proxy.plist" 2>/dev/null || true
launchctl bootstrap gui/$(id -u) "$HOME/Library/LaunchAgents/dev.api678.proxy.plist"
launchctl kickstart -k gui/$(id -u)/dev.api678.proxyCommon issues
| Symptom | Likely cause | Fix |
|---|---|---|
Local /health fails | Proxy not running, Node.js missing, or port conflict | Check terminal logs; use Node.js 18 or later; change port and update client Base URL. |
/v1/models returns 401 | Wrong or disabled real API key | Copy the key again, update api678-proxy.env, and restart the proxy. |
/v1/models returns 403 | Account, group, or model permission issue | Confirm credits and model access; test with a model available in the console. |
| Client still calls official URL | Hermes / OpenClaw did not save settings or cached an old provider | Delete the old provider, add it again, then restart the client or Gateway. |
Bootstrap failed: 5 | The plist file is missing, has invalid XML, still contains placeholder paths, or conflicts with an old service state | Confirm ~/Library/LaunchAgents/dev.api678.proxy.plist exists, run plutil -lint, then run launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/dev.api678.proxy.plist 2>/dev/null || true before bootstrapping again. |
| Slow responses | Slow upstream first token, heavy model, or streaming disabled | Enable streaming, test with a lightweight model, and reduce context length. |