countersign-mcp
v0.1.4
Published
Transaction firewall and intent-verification layer for AI agents transacting onchain. Countersign firewalls the MCP execution path of the OKX OnchainOS stack.
Maintainers
Readme
Countersign
A transaction firewall for AI agents that transact onchain.
Countersign sits between an agent's reasoning and its execution tools. It firewalls the OKX OnchainOS MCP execution path: every transactional tool call is simulated, scanned, policy-checked, and compared against what the human actually asked for — before it can execute. Every decision lands in a tamper-evident audit log.
It is not a wallet, a custody solution, or a key manager. Countersign never holds keys.
Agent ──► Countersign ──► onchainos ──► chain
│
├── does this match what the human asked for?
├── is it within policy?
├── do the scanners flag it?
└── allow · escalate · block → audit logWhy
Wallet-layer policy engines — spend caps, allowlists — are becoming a commodity, and they solve a real problem. But they only see the signed payload. They cannot see that the agent was talked into signing it.
A prompt-injected agent produces transactions that are individually unremarkable: right size, clean token, valid signature, allowed chain. They are wrong only relative to what the user asked for. Catching that requires seeing both the instruction and the decoded transaction, which is what Countersign does.
Here is the case that motivates the whole product. The user asks for USDC. The agent is
induced to buy a token whose ticker is uSDC — a real one, which every risk scanner rates
LOW with zero risk labels:
COUNTERSIGN ESCALATE — human approval required
You asked for: "Buy 5 dollars of USDC on solana"
The transaction: sells WSOL (So111111…111112): risk LOW
buys unverified token EguMF3v7…mapump: risk LOW
Mismatch with your instruction:
! you asked to acquire USDC, but this transaction acquires unverified token
EguMF3v7…mapump — not the canonical USDC address on solanaThe scanners are working correctly. They are just not answering the right question.
Don't take our word for it
One command, against the live endpoint:
npx -y countersign-mcp verify-endpointIt runs four real prompt-injection scenarios and two honest transactions against
https://mcp.countersignai.cloud, using tokens that exist on-chain and scanners running live,
then prints what the firewall did at each step:
ATTACK · counterfeit token
! you asked to acquire USDC, but this transaction acquires unverified token
EguMF3v7…mapump — not the canonical USDC address on solana
✓ CAUGHT
...
attacks caught: 4/4
honest allowed: 2/2
false positives: 0The honest cases matter as much as the attacks: a firewall that refuses everything would score 4/4 and be useless.
Prerequisites: none beyond Node. No wallet, no API key, no OnchainOS identity — the
endpoint is public and free, and the command speaks plain MCP over HTTPS. Point it elsewhere
with verify-endpoint <url> to check your own deployment.
Or check a case of your own
verify-endpoint runs a fixed script. To judge a transaction you choose, give it the
instruction and the token being acquired:
# The real USDC — matches the instruction, so it passes.
npx -y countersign-mcp verify "buy 5 dollars of USDC on solana" \
--to EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
# The look-alike — same instruction, one address different.
npx -y countersign-mcp verify "buy 5 dollars of USDC on solana" \
--to EguMF3v7KgSvQ5iXkF498DtfJQRJZ84ebhnoyymapumpThat is the whole argument in two commands: identical request, one field changed, opposite
verdicts. --chain, --from, --amount and --wallet all have defaults; run countersign
verify with no arguments for the full list. Exit codes are 0 passed, 2 refused, 1 no
verdict — a refusal is a successful run.
Tokens are addresses, never tickers. --to USDC is not accepted, deliberately: a ticker is
chosen by whoever deploys the contract, which is exactly what the look-alike above exploits.
When something misbehaves
countersign info # build, config, deny lists, audit chain, upstream CLI — and what's missing
countersign --help # every command
countersign --version # which build is actually runninginfo answers the questions that otherwise cost an afternoon: which version is running, which
policy file is in effect, whether the deny lists were ever fetched (an unfetched required list
blocks everything, with no obvious cause), and whether the audit chain is intact. It is
read-only, starts no server, and reports credentials as set/not-set without ever printing a
value — so its output is safe to paste into an issue.
If you install via npx, pin the version when it matters — npx -y [email protected] —
because npx will otherwise reuse a cached copy, and a command missing from an older build
falls through to a confusing error rather than the thing you asked for.
Read-only throughout: it asks for verdicts. Nothing is signed and nothing is broadcast — that public endpoint verifies transactions and never executes them, holds nothing spendable, and refuses every tool that could sign or broadcast. See docs/DEPLOY.md for how that is enforced.
Quickstart (5 minutes)
npm install -g countersign-mcp # or use npx directly, below
cp policy.example.yaml policy.yamlPoint your MCP client at Countersign instead of onchainos mcp. There are two ways, and
which you want depends on whether you are firewalling your own execution or just want verdicts.
Hosted — nothing to configure. Verdicts only; it verifies and never executes, so you keep
signing and broadcasting through your own wallet. No keys, no credentials, no onchainos CLI:
{
"mcpServers": {
"countersign": { "url": "https://mcp.countersignai.cloud/mcp" }
}
}Local — the full firewall. Countersign runs on your machine and proxies the whole
OnchainOS tool surface, so transactions are checked and executed through it. Requires the
onchainos CLI on PATH and your OKX credentials
in its environment — without them every transactional call fails closed:
{
"mcpServers": {
"countersign": { "command": "npx", "args": ["-y", "countersign-mcp"] }
}
}Remove any direct onchainos entry — two entries means the agent can choose the unfirewalled
one. Optionally fetch deny lists:
npx -y countersign-mcp refresh-listsThen in your agent session: call set_intent with the user's instruction verbatim, and use the
OnchainOS tools as usual. npx -y countersign-mcp verify-audit checks the log's hash chain.
| Variable | Default | Purpose |
|---|---|---|
| COUNTERSIGN_POLICY | ./policy.yaml | Policy file (schema defaults if absent) |
| COUNTERSIGN_AUDIT | ./countersign.audit.jsonl | Audit log |
| COUNTERSIGN_LISTS | ./.countersign/lists | Deny-list cache |
| COUNTERSIGN_APPROVALS | ~/.countersign/approvals | Out-of-band approval drop |
| ONCHAINOS_BIN | onchainos | Path to the CLI |
| ANTHROPIC_API_KEY | (unset) | Enables the optional LLM second signal |
OKX credentials are read by the onchainos CLI from its own environment. Countersign never
reads, stores, or logs them.
What it checks
| Stage | What happens |
|---|---|
| Intent gate | A verbatim set_intent must be active, unexpired, within its use cap |
| Pre-policy | Chain rules, deny lists (owner's and maintained), token risk on the requested pair |
| Intent check | The requested transaction is compared to the instruction — before upstream builds anything |
| Forward | The upstream tool runs only if the pre-phase allowed it |
| Post-scan | The produced calldata is scanned and simulated |
| Post-policy | Slippage, per-transaction and daily limits, counterparty rules |
| Intent recheck | The decoded outcome is compared to the instruction again |
| Decide | allow · escalate · block, with a plain-language diff |
| Log | Hash-chained JSONL; tampering with any past decision is detectable |
Independent scans run concurrently. Every merge path is fail-closed: a scanner that times out, a simulation that reverts, a broadcast that cannot be traced to scanned calldata — all escalate. A failed scan is never a pass.
One call, one verdict
If you only want an opinion on a single transaction, verify_transaction takes everything at
once and returns the verdict — no session to set up:
{
"instruction": "buy 5 dollars of USDC on solana",
"chain": "solana",
"from_token": "So11111111111111111111111111111111111111112",
"to_token": "EguMF3v7KgSvQ5iXkF498DtfJQRJZ84ebhnoyymapump",
"amount": "5000000",
"wallet": "<your address>"
}Nothing is signed or broadcast — a verdict is an opinion about the transaction, not
permission. Use set_intent plus the OnchainOS tools instead when you want a whole session
firewalled rather than one transaction checked.
Intent verification
Token identity comes from a committed registry of canonical addresses, never from a token's
self-declared name. That is what makes the counterfeit above catchable: a fresh mint can call
itself uSDC, but it cannot occupy the canonical USDC address. Unverified tokens always render
by address — their chosen ticker is never printed as identity.
An optional LLM judge (ANTHROPIC_API_KEY) runs as a second signal only. It can escalate
what the deterministic layer passed; it can never clear a deterministic failure, and it has no
way to express "allow". If unavailable, the deterministic result stands and the rationale
records that the judge did not run.
Measured on the test corpora: 8/8 adversarial attack classes caught (7/8 without the judge), and 36/36 ordinary instructions allowed without a false escalation. Both numbers are regenerated on every test run, because a firewall that escalates normal requests gets switched off, and one that is switched off protects nothing.
Default posture
- Unclassified tools are refused, never forwarded. Countersign ships a reviewed
classification of the upstream surface; anything outside it is escalated. After upgrading
onchainos, new read-only tools escalate until the snapshot is refreshed. That cost is deliberate — a new execution path must never inherit read-only treatment by default. - Buy is stricter than sell. You can always exit a position the buy policy would refuse to enter.
- Calldata is withheld from the agent when post-forward scans fail.
- Unwitnessed signed payloads escalate.
gateway_broadcasttakes an already-signed blob; Countersign does not decode it (shipping an RLP decoder into the trust path means trusting our own parser about what a transaction does). It checks whether the payload embeds calldata it already scanned. No match means unknown provenance —broadcast.unknown_payloadchooses escalate (default) or block. There is deliberately noallow.
Approvals
An escalation issues an approval_id. A human reviews the diff and approves; the agent may
then re-issue the identical call. The approval is bound to the exact arguments and the
active intent, single-use, and expiring — approve a small transfer and the agent cannot execute
a large one. Approval clears an escalation; it never clears a block.
Two channels:
- In-channel (
approve_transaction): convenient, and it proves what was approved — but the MCP channel cannot distinguish a human from the agent, so it does not prove who. - Out-of-band (a file drop the agent has no write access to): create
~/.countersign/approvals, owned by you and not writable by the agent's account. Countersign checks this at startup and warns if the directory is writable by its own account, rather than claiming protection it does not have.
Deny-list screening
Countersign can screen counterparties against maintained deny lists, refreshed explicitly and never fetched at decision time. Its value is verifiable provenance: every decision that consults a list records the list name, version, sha256, and fetch date — on passes as well as matches. That is what lets an audit answer what did you know, from which list version, at the moment you allowed this?
A required list that is missing, stale, or fails its integrity check is a failure, not a skip. A screening control that degrades quietly is worse than none.
This is not sanctions compliance, and Countersign does not claim better coverage than any other tool — only that its coverage is dated, versioned, and auditable. See docs/sanctions-feed-design.md.
Limitations worth knowing before you rely on it
- Token-denominated amounts are not checked by the intent layer. An instruction phrased in
token units — "swap 0.5 ETH for USDC" — has its tokens, chain, and direction verified, but
not its magnitude: comparing token units needs price data the pipeline does not fetch.
An instruction phrased in USD — "swap 50 dollars of ETH" — is amount-checked.
The compensating control is
limits.per_tx_usd, which bounds every transaction regardless of phrasing. Treat these two as a pair: if your instructions are usually token-denominated, setper_tx_usddeliberately, because it is doing the work the intent layer cannot. Tracked by a failing-by-design test in the adversarial corpus; the fix is a month-one item. - The token registry is deliberately small — 11 tokens across 3 chains, each hand-verified
against live chain data. Anything outside it renders as
unverified token 0x…and cannot be confirmed against an intent that names a symbol. That noise is by design, not an oversight: the registry is the trust anchor for the counterfeit check, so entries are a reviewed change rather than an automated import. Broadening it is a tracked growth area. - 95 installed packages from 3 direct dependencies, via the MCP SDK's unused web-server stack. Accounted for honestly in HARDENING.md §4.1; replacing the SDK with direct stdio JSON-RPC is the first post-release milestone.
What Countersign does not do
- No sanctions compliance. See above — screening with provenance is an input to compliance, not compliance itself.
- No key handling. It never sees, stores, or logs keys or credentials.
- No governance of non-MCP routes. An agent with shell access to the
onchainosbinary, its own signing key, or any other execution path is outside what this software governs. Closing those routes is a deployment task, documented as a first-class checklist in HARDENING.md. Read it before production use.
Status
Phase 4 of a phased build; pre-release. The block path is verified against real scanner data
rather than mocks (test/blockTier.test.ts), and the adversarial and honest corpora
(test/adversarial.test.ts, test/honestCorpus.test.ts) are the acceptance tests.
Not implemented, and every decision record says so rather than implying otherwise: live
scanning for the DeFi, payment, gas-station, and competition tool families, and deterministic
protocol-substitution checking (the judge covers it). Token-denominated amount checking is also
absent — see Limitations, and set
limits.per_tx_usd accordingly, because it bounds what an unchecked token-denominated amount
can move.
Engineering notes, including the bugs worth reading about, are in docs/engineering-notes.md.
Reporting a security issue
Email [email protected] — please don't open a public issue for a security report. Non-critical findings are welcome at the same address, and we prefer coordinated disclosure. Scope and details: SECURITY.md.
For anything that isn't security-sensitive, use GitHub issues.
Links
- Source: https://github.com/countersignai/countersign-firewall
- npm:
countersign-mcp— note the package name; plaincountersignon npm is an unrelated abandoned library - Site: https://countersignai.cloud
License
MIT. See LICENSE.
