EPUSDT v2.0.0 接入 OKX 欧易实时汇率教程

发布时间:2026-04-24 07:09 最近更新:2026-09-01 20:14

适用版本

这篇教程适用于 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&quoteCurrency=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 都显示刷新成功
  • 新建订单测试,不要用旧订单判断汇率是否变化

参考链接