678APIdocs
https://www.678api.com/v1
Hermes / OpenClaw

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.

When to use this

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

Hermes / OpenClaw Base URLhttp://127.0.0.1:8787/v1
Hermes / OpenClaw API Keylocal-proxy
Default Windows upstreamhttps://www.wintoken.dev/v1
Default macOS / Linux upstreamhttps://www.678api.com/v1
Real API KeyStored in the local proxy env file

Install Node.js 18 or later, then reopen PowerShell. A standard user account is enough; administrator mode is not required.

Open PowerShell

Press the Windows key, search for “PowerShell,” and open it. Do not run these commands in Command Prompt.

Download and run the installer

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.

text
$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 $installer

Download the Windows installer from wintoken.dev

Optimized for mainland China networks

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:

text
powershell -NoProfile -ExecutionPolicy Bypass -File $installer -UpstreamBaseUrl "https://www.678api.com/v1"

Verify the Windows installation

After installation, run these commands in PowerShell:

text
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

Add an OpenAI-compatible provider

Create an OpenAI Compatible / Custom OpenAI Endpoint in Hermes or OpenClaw provider settings.

Set the local Base URL

Use http://127.0.0.1:8787/v1. Do not append /chat/completions.

Set the local placeholder key

Use local-proxy. Do not enter your real 678API key in the client again.

Save and fully restart the client

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:

text
Get-Content "$env:LOCALAPPDATA\API678\Proxy\api678-proxy.log" -Wait

Change the real API key:

text
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:

text
Get-NetTCPConnection -LocalPort 8787 -ErrorAction SilentlyContinue
Common Windows errors

If “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

text
mkdir -p ~/api678-proxy
cd ~/api678-proxy

Create the proxy script

Create api678-proxy.mjs:

text
#!/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:

text
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.0
Security note

api678-proxy.env contains your real API key. Do not commit it, screenshot it, or share it publicly.

Start and test

text
cd ~/api678-proxy
chmod +x api678-proxy.mjs
node api678-proxy.mjs

Open another terminal and test:

text
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

Add an OpenAI-compatible provider

In Hermes model/provider settings, choose OpenAI Compatible or Custom OpenAI Endpoint.

Set Base URL

http://127.0.0.1:8787/v1

Set API Key

Use local-proxy. The real key is injected by the local proxy from api678-proxy.env.

Select model and enable streaming

Use an available model name from the 678API console. Enable Streaming / Stream for better first-token experience.

OpenClaw settings

Add a custom OpenAI-compatible provider

Create a custom provider in OpenClaw model settings.

Set local endpoint

Use http://127.0.0.1:8787/v1. Do not append /chat/completions.

Set placeholder key

Use local-proxy.

Restart gateway or client

After saving, restart OpenClaw Gateway or reopen the client.

macOS launch at login

To start the proxy automatically after macOS login, create a LaunchAgent:

text
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.proxy

Common issues

SymptomLikely causeFix
Local /health failsProxy not running, Node.js missing, or port conflictCheck terminal logs; use Node.js 18 or later; change port and update client Base URL.
/v1/models returns 401Wrong or disabled real API keyCopy the key again, update api678-proxy.env, and restart the proxy.
/v1/models returns 403Account, group, or model permission issueConfirm credits and model access; test with a model available in the console.
Client still calls official URLHermes / OpenClaw did not save settings or cached an old providerDelete the old provider, add it again, then restart the client or Gateway.
Bootstrap failed: 5The plist file is missing, has invalid XML, still contains placeholder paths, or conflicts with an old service stateConfirm ~/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 responsesSlow upstream first token, heavy model, or streaming disabledEnable streaming, test with a lightweight model, and reduce context length.
© 2026 678API · AI API Gateway Documentation