Skip to content

LeventLabs/Vouchgate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vouchgate — Clean Counterparty

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.

How it works

  • 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.

API

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.

  • riskScore is an integer from 0 (no implemented risk signal matched) to 100.
  • Verdict thresholds (defaults): ≥ 80 → BLOCK, ≥ 50 → FLAG, otherwise PASS.
  • reasons[] explains the verdict; each entry maps to a specific check or an explicit "data unavailable" note.

Advisory explanation

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.

Calling it without an MCP client (direct call)

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.

Payment

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-evm directly against this service's native node:http server — no Express, via a small hand-written adapter (payment.js). GET /paid-ping remains as a self-contained integration-demo route.
  • Requires OKX_API_KEY / OKX_SECRET_KEY / OKX_PASSPHRASE (from the OKX Developer Portal) and X402_PAY_TO_ADDRESS in 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 /mcp closed with 503. For an explicitly unpaid local loopback session, set VOUCHGATE_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.

Run locally

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 identity

Copy .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 use

By 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.js

The 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);

Status

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_call to 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 via tools/refresh-sanctions.mjs; provenance embedded in data/ofac-sdn-evm.json — a point-in-time snapshot, not a live feed, so a PASS means "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 (explanation in the response) that summarizes the verdict in natural language and is structurally unable to change it — see "Advisory explanation" above and layerB.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 /mcp endpoint itself (plus the /paid-ping demo route) — see "Payment" above. Unpaid calls receive a real 402 challenge; 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

  • tools/verify-verdict.mjs — recomputes the canonical evidence hash, checks top-level mirrors against signed evidence, and verifies ed25519. Pin VOUCHGATE_EXPECTED_PUBLIC_KEY or VOUCHGATE_EXPECTED_PUBLIC_KEY_SHA256 out of band to detect substitution of both a response and the service's /signing-key endpoint. 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.
  • 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.md
  • tools/refresh-sanctions.mjs — regenerates the OFAC SDN snapshot from the official-OFAC-derived source lists, with a --check dry-run diff.

Terms & disclaimer

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.

Security model

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.

License

Copyright 2026 LeventLabs.

Licensed under the Apache License 2.0.

About

Counterparty risk screening for autonomous agents, delivered as a pay-per-call service on OKX.AI (X Layer).

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages