Xodian · API v1
Xodian 文档
Xodian 完整手册:快速开始、核心概念、API 参考、组件、令牌验证、Webhook、沙盒以及用户指南。
概览
Xodian 让你的应用确认用户是使用正版手机的一个真实的人,而无需知道他是谁。不需要自拍,不需要上传证件,也不需要个人信息。
用户在 omni 应用中确认。你的后端会收到一个签名的 id_token(ES256 JWT),其中包含:
- human:这是使用未经修改的正版手机的真实用户
- sub:只有你的应用能看到的此人的私密 ID,每次访问都相同
- level:证据强度(1 = 设备,2 = 长期使用)
- uniqueness:此人对你来说是新用户(
new)、回访用户(returning),还是存在冲突(contested)
你会得到
每個應用程式、每位真實人士一個穩定 ID。同一支手機上的 50 個錢包會收斂為一個 ID,換手機後該錢包原有的 ID 仍會保留。只有重新安裝並換新錢包的情況無法回溯連結,此時會標記為 new_device 送出 — 在重要之處請要求等級 2。
你永远不会得到
姓名、面部、证件、手机号、设备型号、余额或活动记录。钱包地址只有在你请求且用户同意时才会提供。
适合的场景
空投和奖励活动(每人一次)、游戏账号和试用、社区投票,以及放在完整 KYC 之前的低成本机器人过滤。
不适合的场景
Xodian 不是身份验证。它不会告诉你用户的姓名、年龄或国家,本身也不能满足 KYC/AML 义务。
快速开始(10 分钟)
你需要一台能保存密钥的服务器,以及用来显示按钮的网页或应用。沙盒免费,无需注册。
获取测试密钥
在首页的沙盒部分或通过 API 获取。密钥只显示一次,请保存在服务器上。
curl -X POST https://xodian.xode.net/v1/test/clients \ -H "Content-Type: application/json" \ -d '{"name":"Acme Airdrop","domain":"acme.example"}' # → {"clientId":"rpt_3f9a…","clientSecret":"osk_rpt_3f9a…_…", # "test":true,"baseUrl":"https://xodian.xode.net","dailyQuota":500}在服务器上创建会话
reference是你为这次尝试设置的 ID(例如领取 ID),会作为令牌中的nonce原样返回。curl -X POST https://xodian.xode.net/v1/sessions \ -H "Authorization: Bearer $XODIAN_SECRET" \ -H "Content-Type: application/json" \ -d '{"scope":["human"],"minLevel":1,"reference":"signup-42"}'{ "sessionId": "BQuIzVWtfYrpFA4_N4ZgzpLW", "pollToken": "CI_8JleZxedaAoDLo7Wg46vB", "expiresAt": 1791251375, "url": "https://xodian.xode.net/s/BQuIzVWtfYrpFA4_N4ZgzpLW", "deeplink": "xode://id?s=BQuIzVWtfYrpFA4_N4ZgzpLW", "scope": ["human"], "minLevel": 1, "test": true }把整个对象发给浏览器即可,其中不含任何密钥。
显示按钮
组件在电脑上显示二维码,在手机上显示"在 omni 中继续"按钮,并轮询状态、回调结果。
<script src="https://xodian.xode.net/sdk.js"></script> <div id="verify"></div> <script> Xodian.button("#verify", () => fetch("/api/xodian/start", { method: "POST" }).then(r => r.json()), { onApproved: (s) => fetch("/api/xodian/finish?session=" + s.sessionId) }); </script>在沙盒中响应
测试会话会在组件内显示模拟面板(批准、拒绝、未通过、过期),也可以调用模拟器 API。
在服务器上获取并验证令牌
用密钥读取会话,再用 JWKS 验证
idToken。参见验证令牌。
rp_… 而非 rpt_…)。生产环境中请拒绝带有 "test": true 的令牌。核心概念
会话
一次验证尝试。由你的服务器创建,用户需在 5 分钟内(expiresAt)在 omni 中响应。每个会话最终只会处于一种状态:
| status | 含义 | 你该做什么 |
|---|---|---|
pending | 等待用户响应 | 保持二维码或按钮可见 |
approved | 已以不低于 minLevel 的等级通过验证 | 在服务器上获取令牌 |
rejected | 用户尝试了但未达标,见 reason | 显示中性提示;omni 会私下告诉用户如何达标 |
denied | 用户拒绝了 | 允许稍后重试 |
expired | 超时无人响应 | 创建新会话 |
保证等级
| level | 要求 | 适用于 |
|---|---|---|
| 1 · device(设备) | 钱包绑定到硬件密钥(StrongBox、TEE 或 Secure Enclave);通过 Google Play Integrity 或 Apple App Attest,且近期重新核验过。拒绝模拟器、已 root 设备和被修改的应用。 | 注册、试用、机器人过滤 |
| 2 · established(长期使用) | 等级 1,加上账号至少 14 天、7 天的已驗證活動(收到帶有硬體金鑰簽章而非僅錢包簽章的心跳的日子)、近 30 天未更换设备。 | 空投、发放奖励、投票 |
minLevel 按会话设置。低于它时你只会收到 rejected,不会收到等级或其他信号。
一人一帳戶是哪個等級?
等級 1 回答「現在是真實手機」。適合機器人過濾、試用與註冊。
若您付費購買的是唯一性,請要求等級 2 — 空投、撥款、投票。在 Android 上硬體金鑰會隨應用程式資料一併刪除,因此重新安裝加上新錢包會呈現為未曾見過的手機。我們不隱藏,而是以 new_device 回報。等級 2 可封閉此情形:在同一支手機上為第二個錢包累積已驗證天數會造成裝置變更,而 30 天內的裝置變更無法通過等級 2。iOS 的金鑰留在鑰匙串中,因此會回傳 returning 或 contested。
应用专属 ID(sub)
同一个人在不同应用中得到不同的 sub,因此合作方无法通过合并数据来追踪一个人。在你的应用内它是固定的:同一个人每次都得到相同的 sub,即使把钱包迁移到新手机也不变。一部手机上的多个钱包得到同一个 sub。请用 sub 作为"每人一次"的判定依据。
唯一性(uniqueness)
| 值 | 含义 |
|---|---|
new | 此人首次在你的应用中验证 |
returning | 之前见过;与上次相同的 sub |
contested | 钱包迁移到了一部已在你的应用中以另一个人身份验证过的手机。由你决定:接受、暂缓审核或拒绝。 |
权限范围(scope)
human 始终包含。如需用户的 XODE 钱包地址,添加 wallet;只有用户在 omni 中开启共享时才会提供,且你的密钥必须已开通此权限。
reference 与 nonce
你传入的 reference(最多 200 个字符)会作为令牌的 nonce 返回。务必核对,防止为一次尝试签发的令牌被用于另一次尝试。
API 参考
基础 URL 为 https://xodian.xode.net,请求和响应均为 JSON。服务器调用使用客户端密钥认证:
Authorization: Bearer osk_<clientId>_<random>
| 方法 | 路径 | 认证 | 用途 |
|---|---|---|---|
| POST | /v1/sessions | 密钥 | 创建会话 |
| GET | /v1/sessions/{id} | 密钥 | 状态;通过后包含令牌 |
| GET | /v1/sessions/{id}/status?t= | 轮询令牌 | 浏览器安全的状态查询(从不返回令牌) |
| GET | /v1/.well-known/jwks.json | 公开 | 签名公钥 |
| GET | /v1/.well-known/configuration | 公开 | 发现文档 |
| POST | /v1/test/clients | 无 | 创建沙盒密钥 |
| POST | /v1/test/sessions/{id}/simulate | 测试会话 | 响应测试会话 |
POST /v1/sessions
| 字段 | 类型 | 默认值 | |
|---|---|---|---|
scope | string[] | ["human"] | human,可选 wallet |
minLevel | 1 | 2 | 1 | 你接受的最低等级(不超过密钥上限) |
reference | string | "" | 你的关联 ID,作为 nonce 返回 |
{
"sessionId": "BQuIzVWtfYrpFA4_N4ZgzpLW",
"pollToken": "CI_8JleZxedaAoDLo7Wg46vB",
"expiresAt": 1791251375,
"url": "https://xodian.xode.net/s/BQuIzVWtfYrpFA4_N4ZgzpLW",
"deeplink": "xode://id?s=BQuIzVWtfYrpFA4_N4ZgzpLW",
"scope": ["human"],
"minLevel": 1,
"test": true
}url 是二维码中的地址;deeplink 在同一部手机上直接打开 omni。请把 sessionId、pollToken、url、deeplink 和 expiresAt 传给浏览器。
GET /v1/sessions/{id}
仅限服务器调用。返回状态;为 approved 时还会返回 idToken、sub 和 level。只有创建该会话的客户端才能读取。
curl https://xodian.xode.net/v1/sessions/BQuIzVWtfYrpFA4_N4ZgzpLW \
-H "Authorization: Bearer $XODIAN_SECRET"
# approved →
{
"sessionId": "BQuIzVWtfYrpFA4_N4ZgzpLW",
"status": "approved",
"expiresAt": 1791251375,
"idToken": "eyJhbGciOiJFUzI1NiIs…",
"sub": "oid_9f3c1a8be41d…",
"level": 1
}
# rejected →
{ "sessionId": "…", "status": "rejected", "reason": "insufficient_level", "expiresAt": … }GET /v1/sessions/{id}/status
供浏览器使用。会话专属的 pollToken 只能看到状态,以及最终状态的原因。组件会每 1 到 3 秒替你调用一次。
GET https://xodian.xode.net/v1/sessions/{sessionId}/status?t={pollToken}
{ "sessionId": "…", "status": "pending", "expiresAt": 1791251375 }发现文档与密钥
GET https://xodian.xode.net/v1/.well-known/configuration
{
"service": "Xodian",
"issuer": "https://xodian.xode.net",
"jwks_uri": "https://xodian.xode.net/v1/.well-known/jwks.json",
"sessions_endpoint": "https://xodian.xode.net/v1/sessions",
"scopes_supported": ["human", "wallet"],
"levels": { "1": "device", "2": "established" },
"id_token_signing_alg_values_supported": ["ES256"],
"session_ttl_seconds": 300
}密钥为 ES256(P-256)。轮换密钥后,旧公钥会在 JWKS 中保留一段时间,因此轮换前刚签发的令牌仍可验证。请缓存 JWKS,遇到未知的 kid 时重新获取。
错误
| HTTP | 何时出现 |
|---|---|
| 400 | 未知的 scope、minLevel 超出范围、请求体格式错误 |
| 401 | 缺少密钥、格式错误或无效 |
| 403 | 你的密钥未开通 wallet 权限 |
| 404 | 会话不存在或属于其他客户端(轮询令牌错误也返回 404) |
| 409 | 会话已结束 |
| 429 | 已达每日验证配额,或今天创建的测试密钥过多 |
错误为 JSON {"detail": "…"},其中 detail 是消息文本,或包含 code 和 message 的对象。
网页组件(sdk.js)
一个无依赖的小脚本,适用于任何页面和任何框架。
<script src="https://xodian.xode.net/sdk.js"></script>
Xodian.button(target, getSession, options)
在 target(选择器或元素)中渲染"Verify with Xodian"按钮。点击后等待 getSession(),该函数必须返回从你的后端获取的会话对象,然后打开弹窗。
<script src="https://xodian.xode.net/sdk.js"></script>
<div id="verify"></div>
<script>
Xodian.button("#verify",
() => fetch("/api/xodian/start", { method: "POST" }).then(r => r.json()),
{ onApproved: (s) => fetch("/api/xodian/finish?session=" + s.sessionId) });
</script>Xodian.open(session, options)
用已有的会话打开弹窗。返回 { close() }。
// Your own button, your own flow:
const session = await fetch("/api/xodian/start", { method: "POST" }).then(r => r.json());
const modal = Xodian.open(session, {
onApproved: (s) => finish(s.sessionId), // fetch the token on YOUR server
onRejected: (s) => explain(s.reason),
onDenied: () => {},
onExpired: () => {},
onClose: () => {}, // closed by the user
});
// modal.close() closes it from code.选项
| 选项 | |
|---|---|
onApproved(s) | 已通过。s 是公开状态;令牌请在服务器上获取。 |
onRejected(s) | 未达标,原因见 s.reason。 |
onDenied(s) · onExpired(s) | 被拒绝或已超时。 |
onClose() | 用户关闭了弹窗。 |
onError(e) | 仅 button:getSession() 失败。 |
label | 仅 button:按钮文字。 |
text | 自定义或翻译弹窗文案(见下文)。 |
autoCloseMs | 成功状态的停留时间(默认 1800)。 |
persona | 仅沙盒:模拟用户的名字。 |
baseUrl | 指定 API 主机(通常自动取自脚本地址)。 |
文案与语言
弹窗默认是英文。传入 text 即可翻译或改成你的语气。键名:title、subtitle、step1、step1m(移动端)、step2、step3、openApp、noApp、secured、okTitle、okBody、deniedTitle、deniedBody、rejTitle、rejBody、expTitle、expBody、close。未提供的键显示英文。
Xodian.open(session, {
text: {
title: "사람 인증",
subtitle: "실제 사람인지 확인해요. 이름·번호·지갑은 공유되지 않아요.",
step1: "omni 앱으로 스캔", step2: "공유 내용을 확인하고 승인", step3: "완료",
close: "닫기",
},
});原生应用
// iOS / Android / Flutter: no QR needed on the same phone.
// Open the deeplink from the session your backend created:
openURL(session.deeplink) // xode://id?s=…
// omni asks the person, then returns them. Your app polls your backend,
// which reads GET /v1/sessions/{id} with the secret./s/<id>)会跟随手机语言显示:英语、韩语、日语或中文。Xodian Connect(錢包)
Connect 是作為 web3 閘道的 Xodian:您的網站無需瀏覽器擴充功能即可連接使用者的 XODE 錢包並請求簽署。復原助記詞不會離開 omni 應用程式,且每一項請求都會在該應用中先向使用者顯示,才會簽署。
流程如下:您的後端開啟工作階段,使用者在 omni 中核准(桌面掃 QR,手機用深層連結),您的頁面取得一次性的 connect 權杖,之後便以該權杖請求簽署。
開啟工作階段
正式用戶端從後端攜帶用戶端密鑰開啟工作階段,只把 sessionId 與 pollToken 交給瀏覽器:
POST https://xodian.xode.net/v1/connect/sessions
Authorization: Bearer osk_<clientId>_<secret>
Origin: https://your-domain.example
Content-Type: application/json
{"clientId":"rp_…","scope":["human","wallet"],"minLevel":1}
Origin 不是憑證
Origin 標頭約束瀏覽器,卻不約束 HTTP 用戶端。若沒有密鑰,任何人都能送出您的網域與(公開的)clientId,以您的名義開啟工作階段,並在有人核准的瞬間取得 connect 權杖。因此正式用戶端必須提供密鑰,否則會收到 403 secret_required。Origin 規則仍在其上繼續適用 — 密鑰說明是誰,Origin 說明來自何處。
沙箱金鑰保留僅瀏覽器的路徑。自助申請的 rpt_ 金鑰可以直接從頁面開啟工作階段,透過您註冊的網域或 http://localhost,因為在那裡無利可圖:真實手機會拒絕沙箱工作階段,因此永遠無法觸及真實錢包。此類工作階段在同意畫面上顯示為未驗證,正式用戶端未帶密鑰所開的工作階段亦同。
SDK
<script src="https://xodian.xode.net/connect.js"></script>
XodianConnect.init({ clientId: 'rp_…', chainId: 3417 });
const { address, evmAddress, sub, level } = await XodianConnect.connect();
XodianConnect.disconnect();
連線資訊(權杖、位址、配對式 sub、等級)保存在頁面自身的 localStorage,有效 30 天。簽署呼叫回傳 401 表示已撤銷或過期,SDK 會清除並重新連線。
探索有兩條路。對 EVM 程式庫,提供者依 EIP-6963 自我宣告為 XODE omni(rdns: net.xode.omni),因此會出現在 wagmi、RainbowKit 與 viem 的清單中,而不必爭奪 window.ethereum — 只有當您想填入該位置且它空著時才使用 init({ injectEthereum: true })。對 polkadot-js 應用,錢包位於 window.injectedWeb3.xodian,提供 enable()、accounts.get() 與 signer。
提供者方法
| 方法 | 回應 |
|---|---|
eth_requestAccounts · eth_accounts | 錢包的 0x 位址,由與其 XODE 帳戶相同的復原助記詞衍生 |
eth_chainId · net_version | 告知頁面的鏈。預設為 XODE 自有的 EVM 層(3417),以 init({ chainId }) 設定您的鏈 |
wallet_switchEthereumChain | 接受任何鏈。連線並不綁定於某一條鏈 — 訊息簽署與鏈無關,而交易自帶 chainId |
personal_sign | 由 0x 帳戶產生的 EIP-191 簽章。手機需 omni 1.2.1 或更新版本 |
| 其他 | 4200 不支援的方法 — 包含尚未提供的 eth_sendTransaction |
值得處理的錯誤
| 代碼 | 意義 |
|---|---|
4001 | 使用者拒絕或關閉了對話框。請顯示為取消,而非失敗 |
4100 | 此連線沒有 0x 位址 — 由較舊的 omni 版本建立。請重新連線 |
4200 | 不支援的方法 |
-32602 | 參數無效(位址錯誤、鏈 id 格式有誤) |
omni 會拒絕什麼,以及為什麼
簽署畫面就是安全邊界,因此凡是無法向使用者說明的,一律拒絕。以下是預期行為,請視為正確:
- 屬於 XODE 自身保留字串的訊息 — 任何以
xode-開頭者。這些字串用於讓錢包向 XODE 自身認證,網站不應能要求簽署。 - omni 無法從原始位元組確認其 pallet 與呼叫索引的呼叫。此判斷在詢問解碼器之前完成。
- 批次包裝。要讀出其中內容需要完整的鏈 metadata,而手機並未攜帶,因此寧可拒絕也不憑信任核准。
- 凡不是 omni 自行從位元組重建的那兩種轉帳的餘額轉帳,一律拒絕。這是擋下「全額轉出」之類的方式。
- 含有 bidi 覆寫或零寬字元的文字 — 以 hex 顯示並附警告,因為這樣的一行讀起來與其內容不同。
- 過長的訊息,在使用者展開之前。超過約 400 個字元或 8 行即會折疊且核准按鈕保持停用,因此藏在畫面下方的要求無法以一次輕觸完成簽署。請讓登入訊息保持簡短。
收據
每一次簽署決定都會記錄於 Sentinel 帳本並錨定於 XODE,與驗證相同。拒絕會附上錢包簽章,因此寫著使用者已拒絕的收據確實代表使用者;未附簽章而關閉的請求則記錄為取消。
上限
| 連線 | 30 天,使用者可隨時在 omni 中撤銷 |
| 簽署請求 | 5 分鐘內回應;每個連線每 5 分鐘最多 30 筆 |
| 訊息大小 | 一般訊息 1 KB,EIP-191 訊息 8 KB |
验证令牌
请务必在服务器上验证,不要相信浏览器报告的状态。
声明(claims)
{
"iss": "https://xodian.xode.net",
"aud": "rp_acme",
"sub": "oid_9f3c1a8be41d02c7aa50e07b",
"iat": 1791251090, "exp": 1791251690,
"jti": "BQuIzVWtfYrpFA4_N4ZgzpLW",
"nonce": "signup-42",
"human": true,
"level": 2, "level_name": "established",
"uniqueness": "new",
"verified_at": 1791251090,
"receipt": "1791251090-a1b2c3d4",
"wallet": "XqF4…" // only with the wallet scope AND the person's consent
}| 声明 | ||
|---|---|---|
iss | 始终为 https://xodian.xode.net | |
aud | 你的客户端 ID | |
sub | 此人在你的应用中的专属 ID | |
exp | 签发后 10 分钟;请立即获取并验证 | |
jti | 会话 ID;保存它以确保每个令牌只能使用一次 | |
nonce | 你的 reference | |
level、level_name | 1 设备 · 2 长期使用 | |
uniqueness | new / new_device / returning / contested | new · returning · contested |
receipt | 已锚定上链的判定记录的句柄(正式密钥) | |
wallet | 仅在有 wallet 权限且用户同意时出现 | |
test | 沙盒令牌为 true;正式令牌中不存在 |
检查清单
- 签名为 ES256 且可通过 JWKS 验证
iss为https://xodian.xode.net,aud为你的客户端 IDexp尚未过期nonce等于创建会话时的referencejti未被使用过- 生产环境中不存在
test level不低于该操作所需的等级
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://xodian.xode.net/v1/.well-known/jwks.json"));
export async function finish(sessionId, expectedReference) {
const s = await fetch(`https://xodian.xode.net/v1/sessions/${sessionId}`, {
headers: { Authorization: `Bearer ${process.env.XODIAN_SECRET}` },
}).then(r => r.json());
if (s.status !== "approved") return { ok: false, reason: s.reason || s.status };
const { payload } = await jwtVerify(s.idToken, JWKS, {
issuer: "https://xodian.xode.net",
audience: process.env.XODIAN_CLIENT_ID,
algorithms: ["ES256"],
});
if (payload.nonce !== expectedReference) throw new Error("token is for another session");
if (payload.test && process.env.NODE_ENV === "production") throw new Error("sandbox token");
await db.usedTokens.insertUnique(payload.jti); // one use per token
return { ok: true, person: payload.sub, level: payload.level,
contested: payload.uniqueness === "contested" };
}import os, requests, jwt # pip install "pyjwt[crypto]" requests
JWKS = jwt.PyJWKClient("https://xodian.xode.net/v1/.well-known/jwks.json")
def finish(session_id, expected_reference):
s = requests.get(f"https://xodian.xode.net/v1/sessions/{session_id}",
headers={"Authorization": f"Bearer {os.environ['XODIAN_SECRET']}"},
timeout=10).json()
if s["status"] != "approved":
return None
key = JWKS.get_signing_key_from_jwt(s["idToken"]).key
claims = jwt.decode(s["idToken"], key, algorithms=["ES256"],
audience=os.environ["XODIAN_CLIENT_ID"],
issuer="https://xodian.xode.net")
assert claims["nonce"] == expected_reference
assert not claims.get("test"), "sandbox token"
return claims # claims["sub"] is your key for this personWebhook
除了轮询,正式密钥还可以在会话进入最终状态时收到 POST 通知。Webhook 在发放正式密钥时一并配置(自助测试密钥不提供)。
| 事件 |
|---|
session.approved(含 idToken)· session.rejected · session.denied |
grant.revoked:用户在 omni 中移除了你的应用(含 sub)。请按你的数据政策处理此人的数据。 |
POST https://your-app.example/xodian/webhook
Content-Type: application/json
User-Agent: xodian-webhook/1
X-Omni-Signature: t=1791251090,v1=5f1c…e9
{"event":"session.approved","sessionId":"…","status":"approved",
"expiresAt":1791251375,"idToken":"eyJhbGciOiJFUzI1NiIs…"}
# when the person removes your app in omni:
{"event":"grant.revoked","sub":"oid_9f3c1a8be41d02c7aa50e07b"}验证签名
请求头格式为 t=<Unix 秒>,v1=<hex>,其中 v1 = HMAC-SHA256(Webhook 密钥, t + "." + 原始请求体)。请拒绝过旧的时间戳以防重放。
import crypto from "node:crypto";
// Use the RAW request body, exactly as received.
function verifyXodian(rawBody, header, secret /* whsec_… */) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`).digest("hex");
const ok = parts.v1 && crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(parts.v1.padEnd(expected.length)));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return ok && fresh;
}投递
请在 8 秒内返回任意 2xx。否则我们会逐步拉长间隔重试,最多共 4 次。通知可能重复,请按 sessionId 幂等处理,并照常验证 idToken。
沙盒
测试密钥(rpt_…)免费且可自助创建。与生产环境一致,但有以下不同:
- 会话通过组件中的模拟面板或 API 的模拟器来响应
- 令牌带有
"test": true,sub以oid_test_开头 - 没有 Webhook,也没有链上凭证
- 每个密钥每天 500 个会话;每个网络每天最多创建 5 个新密钥
模拟器
curl -X POST https://xodian.xode.net/v1/test/sessions/$SESSION_ID/simulate \
-H "Content-Type: application/json" \
-d '{"outcome":"approve","level":2,"persona":"alice","shareWallet":true}'
# outcome: approve | deny | reject | expire
# level: 1 | 2 (approve)
# persona: any name; same persona → same sub
# uniqueness: new | returning | contested (optional, forces the value)
# reason: insufficient_level | device_unverified | no_device | integrity_stale (reject)相同的 persona 总是对应相同的 sub,可用来测试"每人一次"和回访用户。以低于会话 minLevel 的等级批准时,会与生产环境一样判定为未通过。
未通过原因
未通过的会话带有 reason。请向用户显示中性提示,详细原因由 omni 私下告知本人。
| reason | 含义 | 用户可以怎么做 |
|---|---|---|
insufficient_level | 已验证,但低于你的 minLevel | 在这部手机上继续使用 omni;等级 2 随时间达成 |
device_unverified | 手机或应用未通过 Play Integrity / App Attest | 使用未修改的手机和商店版 omni |
integrity_stale | 设备核验已过时 | 打开 omni 重新核验后再试 |
no_device | 该钱包未绑定硬件密钥 | 打开 omni 完成设备设置 |
device_mismatch、device_proof_failed | 响应的手机不是钱包绑定的手机 | 用钱包所在的手机来响应 |
user_denied | 用户拒绝(状态 denied) | 无需处理,可以重试 |
遇到未知原因请按一般未通过处理,今后可能会新增代码。
链上凭证
每一个正式环境的判定(批准或拒绝)都会写入 Sentinel 账本,并每小时锚定到 XODE 链上。令牌中的 receipt 就是该记录的句柄。你或审计方之后可以证明:Xodian 在那个时间点确实做出了这个判定,而且此后无人修改。
GET https://xodian.xode.net/sentinel/receipt/1791251090-a1b2c3d4
GET https://xodian.xode.net/sentinel/anchors
GET https://xodian.xode.net/sentinel/verify.py # independent verifier, standard library only账本是公开的,因此不包含个人信息:对象是哈希值,而不是 sub 或钱包地址。
安全检查清单
- 客户端密钥只放在你的服务器上,不要放进网页、应用包或代码仓库
- 自己验证每一个令牌:签名、
iss、aud、exp、nonce、一次性的jti - "每人一次"的福利以
sub为准,而不是钱包或邮箱 - 上线前决定如何处理
contested(接受、审核或拒绝) - 涉及资金或投票的场景使用等级 2
- 生产环境拒绝
test令牌 - 只保存需要的数据:通常
sub和日期就足够了
限额与密钥
测试密钥 rpt_ | 正式密钥 rp_ | |
|---|---|---|
| 获取方式 | 自助创建 | 联系我们;面向基于 XODE 开发的合作方提前开放 |
| 每日会话数 | 500 | 按合作方约定(默认 10,000) |
| 等级 | 1–2 | 不超过约定的等级 |
| wallet 权限 | 支持 | 开通后支持 |
| Webhook、凭证、认证合作方标识 | — | 支持 |
会话有效期 5 分钟,令牌 10 分钟。正式密钥可应要求轮换;新密钥签发的那一刻旧密钥即失效,请立即部署新密钥。
给你的用户
你可以把本节链接发给用户。用户看到的流程:
安装 omni
在 Google Play 和 App Store 免费下载。omni 是在手机上运行轻节点的 XODE 应用。
扫码或点击
在电脑上,用 omni 应用扫描二维码;在手机上,点击"在 omni 中继续"。
查看共享内容后确认
omni 会显示是哪个应用在请求、以及它将收到什么。确认后会用钱包密钥和手机的硬件密钥签名。
完成
应用只会知道你是真实用户,以及一个只有它能看到的私密 ID。你的姓名、手机号、余额和活动记录永远不会被共享。
管理已连接的应用
在 omni 的设置 → Xodian中,可以查看验证过的应用和当前等级,也可以移除应用。移除后应用会收到通知,但是否删除其已持有的数据由该应用决定,如有需要请直接联系它。
"我没有通过"
omni 会显示原因和解决方法。通常是:在未修改的手机上使用商店版 omni;打开 omni 让它重新核验设备;或者对于要求等级 2 的应用,在同一部手机上继续使用 omni 约两周。
常见问题
这是 KYC 吗?
不是。Xodian 只证明这是使用正版手机的一个真实的人,从不知道他是谁。许多团队把它放在 KYC 之前,只为通过的人支付完整核验的费用。
一个人还能获得多个 ID 吗?
每部正版手机一个。等级 2 要求每部手机持续使用数周,成本很高。Xodian 消除的是廉价手段:一台设备上的大量钱包、模拟器和被修改的应用。
换手机会怎样?
钱包迁移时,你的应用中的 sub 也会保持不变。钱包迁移到已被他人使用过的手机时,会返回 contested。
支持 iPhone 和 Android 吗?
支持。Android 使用 Google Play Integrity,iOS 使用 Apple App Attest。令牌中不会显示是哪个平台。
没有后端可以用吗?
不可以。令牌只会发给持有密钥的服务器,这正是结果可信的原因。
我是通过 app.xode.net/id 接入的,需要修改吗?
旧路径仍然可用。新的接入请使用 https://xodian.xode.net,验证令牌时应期望 iss = https://xodian.xode.net。
更新日志
- 2026-10-06 迁移到
xodian.xode.net,启用短路径(/v1/…、/sdk.js、/s/<id>);签发方变更为https://xodian.xode.net。扫码后的引导页支持 4 种语言。 - 2026-10-05 v1:会话、等级 1–2、应用专属 ID、唯一性、组件、沙盒与模拟器、Webhook、链上凭证。
有问题或需要正式密钥?
最后更新 2026-10-07 · API v1