Files
apex-mcp-server/packages/signer
FilipBl4gojevic 81c14729b0 docs: mainnet cutover complete - both hosted instances run the keyless build (deployed 2026-07-31)
David dispatched the mainnet cutover this morning: deploy-mainnet.yml ran successfully at
2026-07-31T11:00:06Z-11:01:20Z, deploying commit 04e410b (the PR #11 merge, itself the tip of
main after PRs #9/#10/#11 all merged 10:39:29Z-10:39:59Z the same morning). Testnet had already
auto-deployed the identical commit at 10:40:01Z-10:41:50Z. The non-custodial migration is now
complete on both hosted instances, not just in the code - the exact moment the prior docs pass's
deliberately-hedged notice was written for.

Verified directly before writing anything, not taken on faith from the dispatch: git/gh confirm
the merge timestamps and that both deploy workflow runs targeted 04e410b exactly; the custody
grep and safety.ts's absence were reconfirmed on this exact commit; both hosted endpoints
(https://mcp.vector.mainnet.apexfusion.org/sse and the testnet equivalent) were probed directly
with a bare curl request and both returned HTTP 401 right now - meaningful evidence on its own,
since the pre-PR#8 image had no auth at all, so a 401 today proves post-migration code is what's
actually running, not just what the deploy logs claim.

- README.md: the deployment-status callout rewritten with the dated, both-networks-current
  facts, replacing the custody caveat with an access caveat - both instances require a bearer
  token from the operators now (an access control, not a custody signal; neither instance would
  accept a mnemonic from an authenticated caller either, since the tool schemas that would carry
  one no longer exist). The connect command now shows the bearer-token header syntax, verified
  against Claude Code's own current `claude mcp add` documentation before writing it (`--transport
  sse` accepts `--header "Authorization: Bearer <token>"`, same as `--transport http`). The
  security notice rescoped from "this codebase, not every deployment" to a dated claim: complete
  in this codebase and on both hosted instances as of the 2026-07-31 cutover deploy specifically,
  explicitly not a "forever" claim - a future deploy is a separate operational action.
- packages/signer/README.md: the two passages scoped to "this codebase" in the prior docs pass
  now scoped to "any instance running the 2026-07-31 deploy or later," mainnet included, with the
  same access-control caveat about the bearer token.
- docs/architecture/non-custodial-split.md: PR-sequence row 9 (mainnet cutover) gets the same
  landed-marker style as rows 6-8, dated; section 11's rollout paragraph gets its one now-historical
  clause corrected to past tense with a dated annotation appended, following the established
  forward-only annotation convention rather than rewriting the paragraph.

Open public access (no token, per-IP rate limiting) remains a planned follow-up, not yet an open
PR or branch - checked directly (gh pr list, git branch -a) rather than assumed, and stated in
the docs as planned, not implied as already in progress.
2026-07-31 13:14:54 +02:00
..

Vector MCP Signer

Local signing companion for the Vector MCP server. Holds your key; has no network access.

The hosted builder is keyless: it constructs unsigned transactions but cannot sign. This package signs them locally, so your key never reaches a shared server or your model provider.

Why it has no network

  • stdio transport only — no port, no HTTP listener, nothing to connect to
  • no Provider and no Lucid instance — signing uses CML directly
  • submission lives on the builder, so the signer needs no egress either

test/boundary.test.ts checks this mechanically — it walks every file under src/ for a network-capable import or an ambient network global (fetch, WebSocket, ...), and checks package.json for a network-capable dependency. That check is a text scan, not a sandbox, and it is scoped accordingly: it reliably catches accidental regressions and casual misuse — an added dependency, a forgotten forbidden import, an unthinking fetch(...) call, including one routed around the bare identifier via globalThis['fetch'] or globalThis.fetch. It does not catch deliberate obfuscation. Verified directly against the hardened check: an ambient global reached through a plain alias (const f = fetch; f(...)) or a forbidden module specifier assembled at runtime from concatenated strings (import('node:' + 'dns')) both still pass. Closing gaps like those is a code-review responsibility, not something a fixed set of text patterns can guarantee — what this check actually verifies is that today's shipped src/ has no accidental or careless network capability, not a proof that none could ever be smuggled in.

test/integration/roundtrip.test.ts imports @lucid-evolution/lucid and the shared package's /provider — that does not weaken the check above. That file lives under test/, which boundary.test.ts deliberately scans only as far as src/, and which the build never bundles (tsup's only entry point is src/index.ts). Those imports exist so the test can build a real transaction the way the hosted builder does (no key) before handing it to the signer's actual decode/policy/sign code path — the network calls belong to the test's stand-in builder, never to code that ships.

Defense in depth

Beyond "no network," a few structural choices narrow what a bug or an accidental leak can expose:

  • The read-only tools cannot reach the key, even in principle. vector_signer_get_address, vector_signer_decode_transaction, and vector_signer_get_spend_limits are all registered by one function whose parameter list never includes the key source. That's structural, not a matter of discipline: reintroducing signing capability there means visibly changing that function's signature, not adding a line inside a handler.
  • Key material resists accidental logging. TypeScript private is compile-time only — at runtime the field is an ordinary enumerable property, fully visible to JSON.stringify, console.log, and util.inspect. Mnemonics and derived private keys are therefore held in native JavaScript #private fields instead, and both KeySource classes override toJSON() and Node's inspect symbol so that even a stray console.log(keySource) prints a safe description instead of the secret. This defends against accidental disclosure (a debugging log line, a crash reporter serializing local state) — it is not a defense against a compromised process, which can still read the key out of memory directly.
  • The audit log is crash-safe. Every entry is written to a temp file and renamed into place, so a process kill mid-write costs at most the one entry in flight, never the rest of the day's history. See the mandatory-audit limitation below for the cost of this design.

Configuration

Pick one key source:

Variable Notes
VECTOR_SIGNER_MNEMONIC_FILE Path to a file containing your mnemonic. Use chmod 600. Recommended.
VECTOR_SIGNER_MNEMONIC The mnemonic inline. Convenient, but ends up in your shell history and process environment.
VECTOR_SIGNER_PRIVATE_KEY + VECTOR_SIGNER_ADDRESS A derived bech32 signing key. Safest in terms of blast radius — it controls one account rather than your whole HD tree, so a leak is bounded. See the mismatch caveat under Known limitations.

If more than one is set, config.ts checks them in this order: VECTOR_SIGNER_PRIVATE_KEY + VECTOR_SIGNER_ADDRESS, then VECTOR_SIGNER_MNEMONIC, then VECTOR_SIGNER_MNEMONIC_FILE. Unset the ones you are not using — a leftover VECTOR_SIGNER_MNEMONIC in your shell profile silently wins over a VECTOR_SIGNER_MNEMONIC_FILE you just configured.

Optional:

Variable Default Notes
VECTOR_SIGNER_ACCOUNT_INDEX 0 HD account index, for mnemonic sources
VECTOR_SIGNER_SPEND_LIMIT_PER_TX 100000000 (100 AP3X) Refuses any transaction whose net outflow exceeds this
VECTOR_SIGNER_SPEND_LIMIT_DAILY 500000000 (500 AP3X) Refuses once today's signed total would exceed this
VECTOR_SIGNER_AUDIT_LOG_PATH (see note below) Local record of every decision

VECTOR_SIGNER_AUDIT_LOG_PATH, precisely: if unset, the log is written to vector-signer-audit-log.json resolved against the package's own install location (three directories up from build/index.js — the monorepo root, in this repo), not against whatever directory the process happened to be started from. That makes the location predictable regardless of your MCP client's working directory, but it also means the file is easy to forget about. Like the builder's vector-audit-log.json, this exact filename is listed in this repo's .gitignore, so a default-configured signer run from a checkout will not get its audit log accidentally staged. That entry only matches the default filename, though — if you point VECTOR_SIGNER_AUDIT_LOG_PATH at some other path inside the repo, check before you git add, or just set the variable to somewhere outside the repo entirely.

Tools

Tool Purpose
vector_signer_get_address Your address. Pass it to the builder as changeAddress.
vector_signer_decode_transaction Show what a transaction would move, without signing.
vector_signer_sign Policy-check and sign. Returns signed CBOR.
vector_signer_get_spend_limits Limits, today's committed total, recent decisions.

No tool accepts key material as a parameter — that is asserted by the smoke test.

What "net outflow" means

Spend limits apply to net outflow: every output that does not return to your address, plus the fee. Change is excluded, so a 1000 AP3X wallet sending 3 AP3X is measured as ~3.2 AP3X, not 1000.

An output to your own wallet at a different derivation index counts as outflow. That over-counts, which refuses too much rather than too little.

Known limitations

  • Native assets are recorded and reported, but not limited. Spend limits apply to lovelace. A transaction moving ~2 AP3X plus a valuable NFT passes the lovelace check. Asset movements appear in decode_transaction output and the audit log so they are visible, but they are not yet a policy input.
  • The audit log records intent, not confirmation. The signer has no network and cannot observe whether a submit succeeded, so a failed submit still counts against the daily budget. Conservative by design.
  • A durable audit record is mandatory for signing — which means an unwritable audit log stops all signing, not just accounting. AuditLog.append() throws if the write fails, and vector_signer_sign withholds a signature it already produced rather than return one with no durable record of it — the alternative would let a restarted process silently under-count today's spend, which quietly raises the effective daily limit. The practical effect: if the audit log's directory disappears, the disk fills, or the path loses write permission, every vector_signer_sign call starts returning REFUSED — including transactions well inside policy — until it is writable again. That refusal names the audit failure explicitly rather than reading like an ordinary policy refusal, but it is still a full stop on signing, not a warning.
  • The key is plaintext at rest in v1 (env var or file). The win is that it is local and never transits your model provider or a shared host — not that it is in an HSM. KeySource exists so keychain and hardware backends can be added without touching the signing path.
  • VECTOR_SIGNER_PRIVATE_KEY and VECTOR_SIGNER_ADDRESS are not cross-checked against each other. Each is validated as well-formed independently, but nothing confirms the address is actually the one the private key controls — that check is possible entirely offline (this package already depends on the CML library that could do it) but is not wired up yet. A mismatched pair still constructs and signs without error. The failure surfaces later: either as a chain-level rejection at submission, or as the policy engine failing to recognise your own change as change and over-counting it as outflow — the safe direction, but confusing if you hit it without knowing why.
  • Prompt injection is bounded, not eliminated. Policy refuses out-of-limit transactions, but an injected agent can still cause any transaction within policy. Recipient allowlisting is the next lever.
  • Pairing this signer with the builder in this codebase now removes the custody exposure it was designed to fix, for every family the builder exposes. The signer's own four tools are complete and independently verified end to end against live chain data (test/integration/roundtrip.test.ts), and this codebase's builder tools are keyless across the board: every build_* tool, across the wallet/transaction, smart-contract, agent-registry, and self-improvement families, takes a wallet address and never a mnemonic, and vector_submit_transaction / vector_await_transaction complete the round trip. The build → sign → submit → await pipeline is E2E-proven on Vector testnet across all four families: an AP3X self-send (packages/builder/test/integration/keyless-e2e.test.ts), a full register → update → deregister agent lifecycle with the 10 AP3X deposit round-tripped exactly (registry-e2e.test.ts), and a full improvement-proposal lock → spend sequence through the deployed self-improvement module validator, followed by a critique and an endorsement (self-improvement-e2e.test.ts) - the first keyless-built, validated proposal submission that validator has ever accepted. All three E2E suites sign with the same CML primitives sign.ts uses (hash the body, produce a vkey witness, attach it - explicitly a stand-in for calling vector_signer_sign directly, not a call to this package's own code), then submit and confirm on real Vector testnet blocks. This signer's own tool surface, policy engine, and audit log are exercised separately, by this package's own suite (test/integration/roundtrip.test.ts and the smoke tests above). Nothing is left open in the code: this was the last custodial family. The non-custodial migration described in docs/architecture/non-custodial-split.md is complete in this codebase, and as of the 2026-07-31 cutover deploy, on both hosted instances too: pair this signer with a self-hosted builder, the testnet instance, or the mainnet instance now, since any of them running the 2026-07-31 deploy or later carries the guarantee above. Both hosted instances currently require a bearer token from the operators to connect - that is an access control, separate from the custody guarantee above.

Running it

npm run build
VECTOR_SIGNER_MNEMONIC_FILE=/path/to/mnemonic.txt node packages/signer/build/index.js

Or registered directly in an MCP client (Claude Desktop's claude_desktop_config.json, for example):

{
  "mcpServers": {
    "vector-signer": {
      "command": "node",
      "args": ["/path/to/mcp-server/packages/signer/build/index.js"],
      "env": {
        "VECTOR_SIGNER_MNEMONIC_FILE": "/path/to/mnemonic.txt"
      }
    }
  }
}

Register it alongside the builder so an agent has both sets of tools available. This pairing keeps your mnemonic off any server running the 2026-07-31 deploy or later, for every family it exposes: wallet, transaction, smart-contract, agent-registry, and self-improvement alike. Both hosted instances qualify now, and so does any self-hosted deployment of this release. See the last bullet under Known limitations above for the full evidence.

Testing

npm run test:unit          # pure logic, no wallet, no network (from the repo root)
npm run test:smoke:signer  # builds the signer, boots it over stdio, exercises all 4 tools

Both run in CI on every PR.

export VECTOR_SIGNER_MNEMONIC_FILE=/absolute/path/to/testnet-mnemonic.txt
export NODE_OPTIONS=--max-old-space-size=12288
npm run test:integration:signer

Requires a funded Vector testnet wallet. Builds a real unsigned transaction the way the hosted builder does (no key), then decodes, policy-checks, and signs it with the real signer code path against live chain data. Never submits. Not run in CI — see test/integration/roundtrip.test.ts.