Layered trust scoring for x402 agent wallets — combines ERC-8004 reputation, on-chain wallet history, and x402 dispute rates into a single composite score.
npm install @oceanrun/trustgateimport express from 'express'
import { trustGate } from '@oceanrun/trustgate'
const app = express()
app.use(trustGate())Every request with an x-agent-address header gets scored. Trusted agents pass through. Untrusted agents get 403 with reasons. Requests without a wallet header pass through ungated.
There are 20M+ x402 transactions on Base with zero trust layer. Any wallet can call any x402 service. TrustGate sits between the payment and the response: before your service does work for an agent, check whether that agent is worth trusting.
TrustGate produces a 0-10 composite score from three layers:
1. ERC-8004 reputation (60% weight if available, 20% if not) Reads from the on-chain IdentityRegistry and ReputationRegistry. Checks whether the agent is registered, feedback count, and aggregate score from clients.
2. On-chain wallet history (40% weight, or 80% without ERC-8004) Reads from Ethereum mainnet via RPC:
- Wallet age (0-2 pts)
- Transaction count / activity (0-2 pts)
- ETH balance (0-2 pts)
- EOA vs contract (0-2 pts)
3. x402 payment history (0-2 pts, part of on-chain score) Scans USDC transfer logs on Base to/from the x402 facilitator contract. Counts payments and disputes. High dispute rates reduce the score; >5 disputes triggers a block.
https://trust-gate-production.up.railway.app
$0.01 USDC per request on Base mainnet via x402. The /health, /debug/:address, and / endpoints are free.
Returns the full trust profile for a wallet address. Requires x402 payment.
curl https://trust-gate-production.up.railway.app/trust/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045Optional query params: ?threshold=5.0&minStake=200
Response:
{
"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"trusted": true,
"score": 5.4,
"composite": {
"final": 5.4,
"erc8004": 3.8,
"onchain": 7.8,
"breakdown": {
"walletAge": 2,
"activity": 1.5,
"balance": 2,
"x402History": 1.5,
"erc8004": 0.8
}
},
"staked": 3138,
"verified": true,
"reasons": [
"Composite score 5.4 passes threshold 4",
"ERC-8004 stake $3138 meets minimum $100",
"Wallet age 738 days",
"x402: 18 payments, 0.0% dispute rate",
"Identity verified via ERC-8004"
],
"signals": {
"walletAgeDays": 738,
"txCount": 138,
"hasRecentActivity": false,
"isContract": false,
"x402PaymentCount": 18,
"x402DisputeCount": 0
},
"checked_at": "2026-04-14T18:58:11.644Z",
"sources": ["erc8004", "rpc"]
}Allow/deny decision for inline use. Returns 200 if trusted, 403 if not. Requires x402 payment.
curl -X POST https://trust-gate-production.up.railway.app/gate \
-H 'Content-Type: application/json' \
-d '{"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "threshold": 5.0, "minStake": 100}'Raw chain data before scoring. Free, no payment required. Hits real RPCs regardless of MOCK_MODE.
curl https://trust-gate-production.up.railway.app/debug/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045curl https://trust-gate-production.up.railway.app/health{"ok": true, "service": "trust-gate", "version": "0.1.0", "mock_mode": false}git clone https://github.com/oceanrun/trust-gate.git
cd trust-gate
npm install
cp .env.example .env
# Edit .env — set MOCK_MODE=true to start without CDP keys
npm run dev.env variables:
| Variable | Required | Description |
|---|---|---|
MOCK_MODE |
yes | true for local dev, false for live |
WALLET_ADDRESS |
when live | Your Base wallet — receives USDC payments |
CDP_API_KEY |
when live | Coinbase CDP API key ID |
CDP_API_SECRET |
when live | Coinbase CDP API key secret |
ETH_RPC_URL |
no | Ethereum RPC (defaults to publicnode) |
BASE_RPC_URL |
no | Base RPC (defaults to publicnode) |
TrustGate caches every scored address in a trust ledger (Supabase). Scores are cached for 24 hours — repeat checks on the same address return in <500ms instead of 15-20s of RPC calls.
The ledger tracks more than just scores:
pass_count— incremented every time/gatereturns 200 for an address. Agents that pass frequently build a track record.dispute_count— incremented viaPOST /disputewith{"address": "0x..."}. Addresses with >2 disputes are flagged.- Cache misses trigger a full on-chain lookup (ERC-8004 + ETH mainnet + Base x402 logs). The result is written to cache for the next caller.
Over time, the ledger becomes a reputation layer on top of the raw chain data. An agent's TrustGate history — how many services let it through, how many disputed it — feeds back into the trust signal.
Public aggregate data. Cached for 60 seconds.
curl https://trust-gate-production.up.railway.app/stats{
"total_checks": 18,
"paid_checks": 10,
"trusted_rate": 0.5,
"unique_addresses": 1,
"avg_score": 4.8,
"checks_today": 7,
"usdc_earned": 0.1,
"cache_hit_rate": 0.5,
"top_trusted": [{"address": "0x...", "score": 5.4, "pass_count": 12}],
"flagged_addresses": [{"address": "0x...", "score": 2.1, "dispute_count": 5}],
"top_failing_reasons": [{"reason": "No ERC-8004 registration", "count": 9}]
}Flag an address. Free, no payment required.
curl -X POST https://trust-gate-production.up.railway.app/dispute \
-H 'Content-Type: application/json' \
-d '{"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}'If you're building an x402 service and want to gate responses on trust, call /gate before serving your paid response. In your middleware, after the x402 payment clears, extract the payer's wallet address and POST it to TrustGate. If the response is 200, serve the result. If 403, refund or reject. One HTTP call, one boolean — trusted: true or trusted: false. The reasons array tells the caller exactly why they were blocked, and the composite.breakdown gives granular scores if you want to set your own thresholds instead of using the defaults (score >= 4.0, stake >= $100).
MIT