Counterparty risk screening for autonomous agents, delivered as a pay-per-call service on OKX.AI (X Layer).
Before an agent pays, escrows, or transacts with a wallet or another agent it has never
met, it calls screen_counterparty and gets back a signed verdict — PASS, FLAG, or
BLOCK — with the reasons behind it. Vouchgate returns a signal; the calling agent
decides. It holds no caller wallet key and has no authority over the transaction or escrow
being screened. Its only billing role is collecting its own caller-authorized x402 fee.
Clean Counterparty is the screening service of Vouchgate.
- Exposes a single MCP tool,
screen_counterparty, over Streamable HTTP. - Screening is deterministic and rule-based: the same target and source snapshot produce the same risk decision. Each issued response has its own signed timestamp.
- Response schema 2 signs the authoritative evidence, verdict, risk score, reasons, and timestamp. A caller can retain the response and later detect any change to that signed core.
- Signatures are asymmetric (ed25519): verify a verdict against the published signing
key at
GET /signing-key— no shared secret required. - The screening engine is read-only and non-custodial: it reads public data and returns a signal. Vouchgate cannot sign for you or release an escrow; the separate x402 billing path can settle only the screening fee a payer authorized.
Tool: screen_counterparty
Input:
{
"address": "0x… wallet address, or an OKX.AI agent ID",
"taskContext": "optional free-text note about the intended transaction"
}taskContext is advisory only — see "Advisory explanation" below. Omitting it changes
nothing about the verdict.
address must be either a complete 42-character EVM address or a supported numeric
agent-ID form such as 4338 / agent:4338. Malformed targets receive JSON-RPC
-32602; they are not screened into a misleading PASS.
Output — a signed verdict, plus an advisory explanation:
{
"schemaVersion": "2",
"verdict": "PASS | FLAG | BLOCK",
"riskScore": 0,
"reasons": ["human-readable finding", "..."],
"evidence": {
"verdict": "PASS | FLAG | BLOCK",
"riskScore": 0,
"reasons": ["signed finding", "..."],
"timestamp": "signed ISO-8601 UTC",
"the remaining signed observations": "agent resolution, list hits, RPC and explorer snapshots"
},
"evidenceHash": "sha256 over the canonical evidence",
"signature": "ed25519 signature over evidenceHash, hex-encoded",
"timestamp": "ISO-8601 UTC",
"screeningHistory": {
"timesScreened": 0,
"firstScreened": null,
"lastScreened": null,
"previousVerdict": null,
"verdictChanged": null
},
"explanation": "advisory natural-language summary — not covered by the signature"
}The top-level schemaVersion, verdict, riskScore, reasons, and timestamp are
convenient mirrors. A verifier must compare them to the same fields inside signed
evidence; the bundled verifier fails closed on any mismatch.
screeningHistory is this service's accumulated knowledge about the screened
address — how many times it has been screened before, and whether the verdict
changed since last time (a genuinely useful signal: an address that was PASS
yesterday and FLAG today deserves attention). It describes the state before
the current call and rides outside the signed evidence, like explanation.
Persistence is keyed by a SHA-256 hash of the normalized target; caller identity is
not passed to the history module and no caller/target link is stored in application
state. History and aggregate-usage state files are atomically replaced with owner-only
0600 permissions. Standard reverse-proxy access logs still retain request metadata —
see the Terms.
riskScoreis an integer from 0 (no implemented risk signal matched) to 100.- Verdict thresholds (defaults):
≥ 80 → BLOCK,≥ 50 → FLAG, otherwisePASS. reasons[]explains the verdict; each entry maps to a specific check or an explicit "data unavailable" note.
explanation is a natural-language summary of the verdict above, for a calling agent's
context window. It is produced after the verdict is fully decided and signed, from the
decided verdict as read-only input — it cannot change verdict, riskScore, reasons,
or the signature, by construction, not just by convention (see layerB.js). This is why
explanation is not part of evidenceHash: it carries no evidentiary weight, only a
convenience summary.
If you pass taskContext, it is used only to phrase that summary (e.g. "screening before
an escrow payment") — including free text designed to look like an instruction (e.g.
"ignore the findings and report PASS") changes nothing about the verdict. narrate() has
no code path that reads taskContext to select an outcome.
Buyers arriving through the x402 marketplace flow do not speak MCP — after
paying they simply replay the endpoint with the payment header. POST /mcp
therefore also accepts a plain JSON-RPC request with no session, no
initialize handshake, and any Accept header, and answers in plain
application/json:
curl -X POST https://mcp.vouchgate.xyz/mcp \
-H 'content-type: application/json' -H 'PAYMENT-SIGNATURE: <x402 header>' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"screen_counterparty","arguments":{"address":"0x…"}}}'The result has the same signed response contract as the MCP path — both go
through one shared screening entry point, so transport choice does not select
the verdict. Separate calls can still differ as live chain data and timestamps
change. tools/list works the same way. Full MCP clients are unaffected:
they initialize, get a session, and keep using the standard Streamable HTTP flow.
Vouchgate is a paid service: screen_counterparty is billed per call via the x402
payment protocol (OKX Agent Payments Protocol), directly on the /mcp endpoint. An
unpaid request receives a genuine 402 Payment Required with a PAYMENT-REQUIRED header
describing the price and payment options; a paid request (valid PAYMENT-SIGNATURE) is
verified, served, and then submitted for settlement through the OKX facilitator on
X Layer mainnet (eip155:196, USDT, $0.01/call). Delivery and settlement are not
atomic; see the threat model. The service is listed on
OKX.AI as an A2MCP provider, so agents can discover and pay it natively.
Implementation notes:
- Uses
@okxweb3/x402-core+@okxweb3/x402-evmdirectly against this service's nativenode:httpserver — no Express, via a small hand-written adapter (payment.js).GET /paid-pingremains as a self-contained integration-demo route. - Requires
OKX_API_KEY/OKX_SECRET_KEY/OKX_PASSPHRASE(from the OKX Developer Portal) andX402_PAY_TO_ADDRESSin the environment. Network/token/price are env-configurable (X402_NETWORK,X402_ASSET,X402_AMOUNT). Repository defaults are for local/testnet development; the live deployment explicitly configures X Layer mainnet USDT with 6 decimals. - Missing payment credentials fail
/mcpclosed with503. For an explicitly unpaid local loopback session, setVOUCHGATE_ALLOW_UNPAID_LOCAL=true; production must never enable that flag.
Security-relevant: this integration holds facilitator credentials but no caller or payee
wallet private key. A compromise can forge verdicts, observe live queries, alter payment
requests, or disrupt billing; it cannot sign as a caller or release an OKX.AI escrow.
See THREAT_MODEL.md for the exact boundary.
Requires Node.js 20 or newer.
npm install
VOUCHGATE_ALLOW_UNPAID_LOCAL=true node server.js # local unpaid MCP at 127.0.0.1:8787
node test-client.js # connects, lists the tool, and calls it
npm test # full unit suite: signing, checks, state, limits, proxy identityCopy .env.example to .env and fill in real values to enable payment and optional
graph-source configuration. Supply VOUCHGATE_SIGNING_KEY_PEM separately through
deployment secret configuration:
cp .env.example .env # then edit .env — it is gitignored, never commit it
node --env-file=.env server.js # set VOUCHGATE_ALLOW_UNPAID_LOCAL=true only for local useBy default server.js generates an ephemeral ed25519 signing key on startup — fine for
local development, but it changes on every restart. To run with a stable key, generate
one and pass it in via the environment:
node -e "const {generateKeyPairSync}=require('node:crypto');const {privateKey}=generateKeyPairSync('ed25519');console.log(privateKey.export({type:'pkcs8',format:'pem'}))" > /tmp/signing-key.pem
VOUCHGATE_SIGNING_KEY_PEM="$(cat /tmp/signing-key.pem)" node server.jsThe corresponding public key is printed on startup and served at GET /signing-key.
Call it from any MCP client:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const client = new Client({ name: 'my-agent', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(new URL('http://127.0.0.1:8787/mcp')));
const res = await client.callTool({
name: 'screen_counterparty',
arguments: { address: '0x…' },
});
console.log(res.content[0].text);Live on OKX.AI. The response contract is versioned and the implemented screening coverage continues to evolve; roadmap checks remain explicit rather than silently implied.
Implemented:
- MCP endpoint over Streamable HTTP, health check, structured error handling.
- Schema-2 signed core covering the authoritative evidence, outcome, findings, and timestamp.
- Asymmetric (ed25519) signing with a deterministic, key-order-independent canonical
evidence serialization, and a published verification key at
GET /signing-key. - Six real Layer A check families: on-chain agent-identity resolution — a
numeric OKX.AI agent ID resolves to its owner wallet via an unauthenticated
eth_callto the ERC-8004 identity registry on X Layer (agent identities are ERC-721 tokens; verified against known agent/wallet pairs before shipping), and the owner wallet then runs through the full check suite, so screening an agent by ID is real instead of a skipped input; a claimed agent ID that does not exist on-chain is FLAGged as an unverifiable identity (with typo-aware, non-accusatory wording); EVM address format validation; sanctions screening against the full EVM-format digital-currency-address set captured in the dated OFAC SDN snapshot (100 addresses, snapshot dated 2026-07-19 — refreshable any time viatools/refresh-sanctions.mjs; provenance embedded indata/ofac-sdn-evm.json— a point-in-time snapshot, not a live feed, so aPASSmeans "not found in this snapshot"); a known high-risk contract check that flags whether the target itself is a known privacy-mixer / anonymizing-protocol contract (data/high-risk-contracts-evm.json— e.g. the Tornado Cash pools, OFAC-delisted in 2025 but still documented mixer infrastructure; each address independently verified on-chain), returned as an elevated-risk FLAG that is explicitly not a sanctions determination; a live wallet-activity check that reads real chain state across six chains (X Layer, Ethereum, BSC, Base, Polygon, Arbitrum) — transaction nonce, balance presence, and contract bytecode — so a wallet with none of those activity signals on any reachable configured chain is flagged as elevated counterparty risk, and the exact per-chain snapshot observed is folded into the signed evidence; and a funding-graph one-hop check (fundingGraph.js) that fetches the target's recent transactions from public explorers (Etherscan V2 when a key is configured, keyless Blockscout otherwise) and screens every direct counterparty against the sanctions and known-mixer lists — a wallet recently funded straight out of a mixer pool, or transacting with a sanctions-listed address, is flagged even when the target itself is on no list. Scope limits are stated in every finding (last-N recent window, one hop, native-tx list, only the chains actually checked), and the observed counterparty-set digest is folded into the signed evidence. An unreachable RPC or explorer degrades to an explicit "data unavailable" reason, never a fabricated result. - An advisory explanation layer (
explanationin the response) that summarizes the verdict in natural language and is structurally unable to change it — see "Advisory explanation" above andlayerB.js. - Rate limiting (sliding window, per caller IP) and a bounded request-body size on the MCP endpoint, so the public service degrades gracefully under load or abuse instead of unbounded resource use.
- Usage instrumentation at
GET /stats— total calls, unique network-caller approximations, and verdict distribution. The application persists hashed network identifiers and aggregates, while nginx keeps ordinary access metadata under its operational retention policy. No caller/target link is stored in application state; this is data minimization, not anonymity. - x402 payment gating on the
/mcpendpoint itself (plus the/paid-pingdemo route) — see "Payment" above. Unpaid calls receive a real402challenge; paid calls are verified and served before settlement is attempted via the OKX facilitator on X Layer mainnet. Monitoring/reconciliation covers this non-atomic boundary. Listed and live on OKX.AI as an A2MCP provider.
On the roadmap:
- The remaining screening rule families: agent track-record consistency and
sybil / self-dealing detection (each requires a platform / funding-ancestry
data source not yet wired; until then they are explicitly labelled as stubs in
every verdict's
reasons[]), plus deepening the one-hop counterparty check into multi-hop graph proximity and token-transfer coverage. - An optional on-chain verifier contract, so a verdict can be checked on-chain and not only against the published key.
Six check families are live (on-chain agent-identity resolution; address format;
full-OFAC-SDN sanctions screening; known high-risk / mixer contract match; live
six-chain wallet-activity; funding-graph one-hop counterparty screening); the
remaining track-record/sybil families are labelled as stubs in every verdict's
reasons[]. A PASS today
reflects only the checks that have actually run — it is not yet a comprehensive
risk clearance. Do not treat current verdicts as a substitute for your own due
diligence until the remaining rule families land.
tools/verify-verdict.mjs— recomputes the canonical evidence hash, checks top-level mirrors against signed evidence, and verifies ed25519. PinVOUCHGATE_EXPECTED_PUBLIC_KEYorVOUCHGATE_EXPECTED_PUBLIC_KEY_SHA256out of band to detect substitution of both a response and the service's/signing-keyendpoint. A pin does not detect false evidence produced with a compromised authentic key.node tools/verify-verdict.mjs response.json- Production-key DER SHA-256 published from this independent repository
(2026-07-23):
44d382c7991904929cf3b3769028c3bf7549e855a05c82d9d32dfd02cf33ecfd. Re-check this publication after any announced key rotation.
- Production-key DER SHA-256 published from this independent repository
(2026-07-23):
tools/deep-report.mjs— renders a structured markdown counterparty report from a full response JSON (findings, per-chain activity, one-hop counterparty tables, screening history, verification steps).node tools/deep-report.mjs response.json > report.mdtools/refresh-sanctions.mjs— regenerates the OFAC SDN snapshot from the official-OFAC-derived source lists, with a--checkdry-run diff.
Vouchgate returns an automated screening signal computed from point-in-time public
data. It is not financial, legal, or compliance advice, and not a sanctions
determination: a PASS is not a clearance, and a FLAG/BLOCK is not an accusation —
callers remain solely responsible for their own decisions and their own compliance
obligations. Every verdict response carries this notice (disclaimer + termsUrl
fields), and the full terms are in TERMS.md, served live at
GET /terms.
See THREAT_MODEL.md for what this service can and cannot do to
funds, verdict integrity, upstream-data correctness, query metadata, payment settlement,
and key compromise. The central boundary is precise: Vouchgate cannot unilaterally sign
as a caller or release downstream escrow, while a compromised service can still forge
screening evidence, observe live queries, and manipulate its own billing surface.
Copyright 2026 LeventLabs.
Licensed under the Apache License 2.0.