EPUSDT v2.0.0 接入 OKX 欧易实时汇率教程
适用版本
这篇教程适用于 epusdt v2.0.0。作用是让后台自动汇率使用 OKX 欧易的 USDT/CNY 实时价格,同时补齐 TRX、SOL、BNB、USDC、USDC.e、TON 等常用资产汇率。
v2.0.0 后台会校验汇率 API 地址,不能填写 127.0.0.1、localhost 或内网 IP。正确做法是:汇率服务只监听本机,再用当前 epusdt 域名反向代理一个公网 HTTPS 路径给后台填写。
1. 创建汇率服务目录
在 epusdt 所在服务器执行:
mkdir -p /opt/epusdt-okx-rate cd /opt/epusdt-okx-rate
2. 新建 server.py
新建文件 /opt/epusdt-okx-rate/server.py,内容如下:
#!/usr/bin/env python3
import json
import logging
import re
import threading
import time
import urllib.parse
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from typing import Dict, Optional, Tuple
HOST = "127.0.0.1"
PORT = 18089
CACHE_TTL_SECONDS = 60
USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/135.0.0.0 Safari/537.36"
)
OKX_EXCHANGE_URL = "https://www.okx.com/zh-hans/exchange/usdt-to-cny"
OKX_CONVERT_URL = "https://www.okx.com/zh-hans/convert/cny-to-usdt"
OKX_QUOTED_PRICE_URL = (
"https://www.okx.com/priapi/v3/b2c/deposit/quotedPrice"
"?baseCurrency=USDT"eCurrency=CNY"
)
PUBLIC_CURRENCY_API_URL = (
"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/"
"currencies/{base}.json"
)
COINGECKO_TON_PRICE_URL = (
"https://api.coingecko.com/api/v3/simple/price"
"?ids=the-open-network&vs_currencies={base}"
)
PRODUCT_PRICE_RE = re.compile(r'"price":"([0-9]+(?:\.[0-9]+)?)","priceCurrency":"CNY"')
DIRECT_RATE_RE_LIST = (
re.compile(r'当前 1 CNY 可兑换 ([0-9]+(?:\.[0-9]+)?) USDT'),
re.compile(r'CNY/USDT 今天的兑换率为 ([0-9]+(?:\.[0-9]+)?) USDT'),
)
_okx_cache_lock = threading.Lock()
_okx_cache: Dict[str, object] = {"ts": 0.0, "rate": None, "source": None}
_public_cache_lock = threading.Lock()
_public_cache: Dict[str, dict] = {}
def http_get_text(url: str, timeout: int = 8) -> str:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.read().decode("utf-8", errors="ignore")
def fetch_rate_from_exchange_page() -> Tuple[float, str]:
html = http_get_text(OKX_EXCHANGE_URL)
match = PRODUCT_PRICE_RE.search(html)
if match:
price_cny_per_usdt = float(match.group(1))
if price_cny_per_usdt > 0:
return round(1.0 / price_cny_per_usdt, 12), "okx-exchange-page"
for regex in DIRECT_RATE_RE_LIST:
match = regex.search(html)
if match:
return round(float(match.group(1)), 12), "okx-exchange-page-faq"
raise ValueError("failed to parse OKX exchange page")
def fetch_rate_from_convert_page() -> Tuple[float, str]:
html = http_get_text(OKX_CONVERT_URL)
match = PRODUCT_PRICE_RE.search(html)
if match:
price_cny_per_usdt = float(match.group(1))
if price_cny_per_usdt > 0:
return round(1.0 / price_cny_per_usdt, 12), "okx-convert-page"
for regex in DIRECT_RATE_RE_LIST:
match = regex.search(html)
if match:
return round(float(match.group(1)), 12), "okx-convert-page-faq"
raise ValueError("failed to parse OKX convert page")
def fetch_rate_from_quote_api() -> Tuple[float, str]:
payload = json.loads(http_get_text(OKX_QUOTED_PRICE_URL))
items = payload.get("data") or []
if not items:
raise ValueError("quotedPrice returned empty data")
price_cny_per_usdt = float(items[0]["price"])
if price_cny_per_usdt <= 0:
raise ValueError("quotedPrice returned invalid price")
return round(1.0 / price_cny_per_usdt, 12), "okx-quotedPrice"
def fetch_okx_cny_to_usdt_rate() -> Tuple[float, str]:
for fetcher in (
fetch_rate_from_exchange_page,
fetch_rate_from_convert_page,
fetch_rate_from_quote_api,
):
try:
return fetcher()
except Exception:
continue
raise RuntimeError("all okx fetch failed")
def get_okx_cny_to_usdt_rate(force_refresh: bool = False) -> Tuple[float, str]:
now = time.time()
with _okx_cache_lock:
rate = _okx_cache["rate"]
source = _okx_cache["source"]
if (
not force_refresh
and rate is not None
and source is not None
and now - float(_okx_cache["ts"]) < CACHE_TTL_SECONDS
):
return float(rate), str(source)
rate, source = fetch_okx_cny_to_usdt_rate()
_okx_cache["ts"] = now
_okx_cache["rate"] = rate
_okx_cache["source"] = source
return rate, source
def fetch_ton_rate_for_base(base: str) -> Optional[float]:
payload = json.loads(http_get_text(COINGECKO_TON_PRICE_URL.format(base=base)))
price = (payload.get("the-open-network") or {}).get(base)
if price is None:
return None
price = float(price)
if price <= 0:
return None
return round(1.0 / price, 12)
def apply_epusdt_aliases(base: str, data: dict) -> dict:
normalized = dict(data)
if "usdc" in normalized and "usdc.e" not in normalized:
normalized["usdc.e"] = normalized["usdc"]
if "ton" not in normalized:
try:
ton_rate = fetch_ton_rate_for_base(base)
if ton_rate is not None:
normalized["ton"] = ton_rate
except Exception:
logging.exception("failed to fetch TON rate for %s", base)
return normalized
def fetch_public_currency_payload(base: str) -> dict:
payload = json.loads(http_get_text(PUBLIC_CURRENCY_API_URL.format(base=base)))
data = payload.get(base)
if not isinstance(data, dict):
raise ValueError(f"invalid currency payload for {base}")
return apply_epusdt_aliases(base, data)
def get_public_currency_payload(base: str, force_refresh: bool = False) -> dict:
now = time.time()
with _public_cache_lock:
cached = _public_cache.get(base)
if cached is not None and not force_refresh and now - cached["ts"] < CACHE_TTL_SECONDS:
return dict(cached["data"])
data = fetch_public_currency_payload(base)
with _public_cache_lock:
_public_cache[base] = {"ts": now, "data": dict(data)}
return dict(data)
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
class Handler(BaseHTTPRequestHandler):
server_version = "epusdt-okx-rate/2.0"
def do_GET(self) -> None:
parsed = urllib.parse.urlparse(self.path)
force_refresh = urllib.parse.parse_qs(parsed.query).get("refresh") == ["1"]
if parsed.path == "/healthz":
self.write_json(200, {"ok": True})
return
if parsed.path == "/":
self.write_json(
200,
{
"service": "epusdt okx rate adapter",
"usage": {
"cny": "GET /cny.json",
"usd": "GET /usd.json",
"healthz": "GET /healthz",
},
},
)
return
if not parsed.path.endswith(".json"):
self.write_json(404, {"error": "not found"})
return
base = parsed.path.rsplit("/", 1)[-1][:-5].strip().lower()
if not base:
self.write_json(404, {"error": "not found"})
return
try:
data = get_public_currency_payload(base, force_refresh=force_refresh)
extra_headers = None
if base == "cny":
rate, source = get_okx_cny_to_usdt_rate(force_refresh=force_refresh)
data["usdt"] = rate
extra_headers = {"X-Rate-Source": source}
elif base == "usd":
data["usdt"] = 1
self.write_json(200, {base: data}, extra_headers)
except Exception as exc:
logging.exception("failed to serve %s", parsed.path)
self.write_json(500, {"error": str(exc)})
def log_message(self, fmt: str, *args) -> None:
logging.info("%s - %s", self.address_string(), fmt % args)
def write_json(
self,
status: int,
payload: dict,
extra_headers: Optional[Dict[str, str]] = None,
) -> None:
body = json.dumps(payload, ensure_ascii=True, separators=(",", ":")).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
if extra_headers:
for key, value in extra_headers.items():
self.send_header(key, value)
self.end_headers()
self.wfile.write(body)
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
server = ThreadingHTTPServer((HOST, PORT), Handler)
logging.info("listening on http://%s:%s", HOST, PORT)
server.serve_forever()
if __name__ == "__main__":
main()
这份脚本只使用 Python 标准库,不需要安装 pip 依赖。USDT/CNY 使用 OKX 实时价格,其它币种用公共汇率源补齐;TON 使用 CoinGecko 的 the-open-network 价格补齐,USDC.e 和 USDC 同价。
3. 新建 systemd 服务
新建文件 /etc/systemd/system/epusdt-okx-rate.service:
[Unit] Description=EPUSDT OKX Rate Adapter After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/bin/python3 /opt/epusdt-okx-rate/server.py WorkingDirectory=/opt/epusdt-okx-rate Environment=PYTHONUNBUFFERED=1 Restart=always RestartSec=2 [Install] WantedBy=multi-user.target
4. 启动并检查
执行:
python3 -m py_compile /opt/epusdt-okx-rate/server.py systemctl daemon-reload systemctl enable --now epusdt-okx-rate.service systemctl status epusdt-okx-rate.service --no-pager curl http://127.0.0.1:18089/healthz curl http://127.0.0.1:18089/cny.json curl http://127.0.0.1:18089/usd.json
cny.json 里至少要看到 usdt、trx、usdc、ton;usd.json 里 usdt 应为 1。
5. 配置 Nginx 反向代理
在 epusdt 域名的 HTTPS server 配置里,把下面这段放到主站 location / 前面:
location ^~ /okx-rate-api/ {
proxy_pass http://127.0.0.1:18089/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30;
proxy_send_timeout 30;
}
保存后检查并重载:
nginx -t systemctl reload nginx curl https://你的epusdt域名/okx-rate-api/cny.json curl https://你的epusdt域名/okx-rate-api/usd.json
这两个 HTTPS 地址必须返回 JSON,不能显示 epusdt 前端页面。
6. 后台填写汇率配置
进入 epusdt 后台 系统配置 -> 支付配置,把汇率相关配置改成:
汇率模式:auto 汇率 API 地址:https://你的epusdt域名/okx-rate-api 缓存时间:300
汇率 API 地址 不要填 http://127.0.0.1:18089/。在 v2.0.0 里这样会保存失败,并提示 rate.api_url 保存失败:无效配置项。
强制汇率列表 是固定汇率或兜底配置;要使用实时汇率,就把汇率模式设为 auto,让系统读取上面的 API 并写入汇率缓存。旧字段 rate.forced_usdt_rate 不再作为这篇教程的主要方案。
7. 资产和网络说明
USDT在 TRON、BSC/binance、Polygon、Solana、TON、Aptos 等网络都会读取同一个usdt汇率。TRX读取trx汇率,只影响 TRON 原生 TRX 支付。USDC读取usdc汇率;Polygon 的USDC.e读取usdc.e,本脚本按USDC同价返回。SOL、BNB、APT等资产只要后台启用了对应链和代币,就会按 JSON 里的同名小写字段取价。TON原生币读取ton,TON 链上的 USDT 仍读取usdt。
8. 最后检查
systemctl status epusdt-okx-rate.service显示active (running)https://你的epusdt域名/okx-rate-api/cny.json返回 JSON- 后台保存
rate.api_url不再报无效配置项 - 后台刷新汇率缓存后,
cny和usd都显示刷新成功 - 新建订单测试,不要用旧订单判断汇率是否变化