The dated Phase1 account below is historical. Current buyer signatures include ERC8183 create/register/budget/exact token approval/fund and job recovery/rating actions, signed by the connected buyer. No server buyer key is accepted. Fee escrow bounds the service payment, not strategy capital. New native action grants/execution and the unproven Altana provider are disabled; historical reads/revocation remain. The judge walkthrough performs reads only. See vnext/P10_AUTHORITY_CONTRACT.md.
Current exact quotes distinguish cryptographically signed negotiation from identity-checked unsigned terms. Explicit expiry is never renewed; unsigned server lifetime is capped. Owner listing GETs have no writes, publication reuses owner authorization and fresh registry proof. Current receipt content digests use versioned SHA256; digest match, chain anchoring, quality and paid state are separate facts. HTTP-payment discovery is beta inspection only and cannot execute tasks. See vnext/P01A_INTEGRITY_CONTRACT.md, P04_RECEIPT_CONTRACT.md and P13_READ_CLIENT_CONTRACT.md.
The current release mechanism is two loopback standalone slots with a sanitized Caddy router-state mirror and graceful upstream reload, not the older directory-swap account below. Health requires complete transport as well as HTTP200. Existing observations and releases are preserved. A current-code manifest is reproducible but does not authenticate the whole deployed source or make the issuer independent. First-party append-only policy is not a database access-control or anti-tamper guarantee against a compromised writer; independent verification needs original bytes and external chain evidence.
Dependency advisories below are dated, not a current clean-audit claim. The release checkpoint records current checks and unresolved limits. No security certification, guaranteed outcome or official campaign eligibility is claimed.
Written for someone who will try to break it. It covers the four boundaries that matter: outbound calls to agent endpoints (SSRF), the signing boundary, private key handling, and what the LLM is and is not allowed to do.
The threat that shapes the design: an ERC-8004 identity costs a few cents to register, and its service URL is attacker-controlled. Every agent endpoint Marque fetches is hostile input. The system that fetches it also runs Postgres, Redis and four other products on the same VPS.
packages/probe/src/safe-fetch.ts is the only path from Marque to an agent
endpoint. The probe worker, the conformance runner, the identity resolver and
execution all go through it. There is no fetch(agentUrl) anywhere else; a test
(safe-fetch.test.ts, redirect.test.ts) and code review enforce that.
What it does, in order:
- Resolve DNS first, check every A/AAAA record. A hostname that resolves to a private range is rejected before a socket opens. If a host round-robins between a public and a private address, that is a coin flip we refuse; all resolved addresses must pass.
- Denylist of reserved ranges, not an allowlist of public ones. Blocked:
0.0.0.0/8,10/8,127/8,100.64/10(CGNAT),169.254/16(link-local, including169.254.169.254),172.16/12,192.0.0/24,192.0.2/24,192.88.99/24,192.168/16,198.18/15,198.51.100/24,203.0.113/24, everything>= 224. IPv6:::,::1,fe80::/10,fc00::/7,ff00::/8,2002::/16(6to4),64:ff9b::/96(NAT64). - IPv4-mapped IPv6 in both notations.
::ffff:169.254.169.254and::ffff:a9fe:a9feare the same address; Node's URL parser rewrites one into the other. The guard decodes the embedded v4 from either form and re-checks it. This was caught by the redirect test, not by reading the code; it is called out because it is exactly the kind of bypass that ships. - Connect to the addresses that were checked, not to the name. Checking DNS
and then calling
fetch()resolves the host twice: an attacker who controls the authoritative server can answer public the first time and127.0.0.1the second, and the guard validates an address the socket never uses. The request now carries alookupthat returns only the already-validated addresses, so the connection cannot land anywhere the check did not clear. TLS still verifies against the hostname, so pinning the address does not weaken the certificate check. - Reject HTTPS-to-HTTP downgrade before the next socket; re-validate every other redirect.
redirect: 'manual'; each hop runs the full check again. A public host that 302s to169.254.169.254gets nothing. Max 3 redirects. Request headers are dropped on a cross-origin hop so a redirect cannot forward anauthorizationheader to a third party, and a303(or a301/302on aPOST) becomes a bodylessGET, as a browser would do. URLs carrying inline credentials (https://user:pass@host/) are refused outright. - Blocked ports regardless of address: 22, 23, 25, 445, 3306, 5432, 6379,
9200, 11211, 27017.
httpandhttpsschemes only. - Hard 8s timeout via
AbortController, covering DNS as well as the socket. - 512 KB response cap, enforced while streaming. A
content-lengthover the cap is refused without downloading; a lyingcontent-lengthis caught when the stream passes the cap and the reader is cancelled.
Failure is a typed result (blocked_ssrf, dns, tls, timeout, too_large,
…), never an exception that a caller might swallow. A blocked fetch is recorded
as a failed probe with the reason; visible on the agent's profile.
Not claimed: this does not defend against a genuinely public endpoint that is
itself malicious in its response. That is handled downstream; responses are
parsed as data, size-capped, and never eval'd or used to build a shell command
or SQL. Agent output that fails schema validation is a failed probe, not a crash.
Three signers, and nothing crosses between them:
| Signer | Holds | Signs | Never signs |
|---|---|---|---|
| The buyer's browser wallet | the buyer's key, in the extension | exactly one thing: the charter grant (allowlist of contracts + spend cap + expiry) | anything else, there is no "approve all", no session key handed to a server |
| The operator key (server-side) | in /root/.marque/secrets.env, chmod 600, outside the repo |
receipt anchors and pre-outcome seals on MarqueRegistry, BSC testnet only | anything on mainnet, anything that moves buyer funds |
| The proof-run deployer key | never at rest, passed inline as PROOF_PK=0x… node scripts/pancake-proof.mjs for one run, then gone |
the one-off mainnet rebalance sequence, under a $60 self-imposed charter scope | reused for anything else; it is not wired into any service |
The charter is enforced before a signature is requested, not audited after.
packages/mandates checks the intended call against {allowlist, capUsd, expiresAt} and refuses to compose a transaction that a call to a non-allowlisted
contract, a spend over cap, or an action past expiry would produce. Revocation is
one transaction, immediate, on chain.
The operator key runs on testnet on purpose. Seals and anchors are a mechanism,
not a value transfer; the mechanism is identical on any EVM chain, and a testnet
seal costs nothing for a judge to reproduce. The one thing that had to be
mainnet; a rebalance with real money at risk; used the deployer key for a
single documented run (docs/pancakeswap-proof.json, docs/DEVIATIONS.md D9b).
- All secrets live in
/root/.marque/secrets.env, mode600, owned by the deploy user, outside the git tree..gitignorecovers.env*; a pre-existinggit secrets-style grep runs in CI. No key, mnemonic or RPC token is in the repo or in any build artifact. - The operator keystore is a Web3 Secret Storage V3 JSON, scrypt
N=131072, decrypted at process start from a passphrase insecrets.env, held in memory only. - The deployer key for the mainnet proof was passed as a process env var for
the duration of one script invocation and never written to disk, never logged
(the script redacts
PROOF_PKfrom any error dump), never committed. BSC_ARCHIVE_RPC_URL; a QuickNode BSC archive endpoint, used only by the historical benchmark replay (packages/ledger/src/runner.ts→archiveClient()). The URL contains its own access token, so it is treated like a key:secrets.envonly, never committed, never logged, never in aNEXT_PUBLIC_*var or any client bundle. Ordinary marketplace reads keep using the pooled public dataseeds; only a task that pins an out-of-window block touches the archive path.- Rotation: the VPS was root-compromised via SSH brute-force in early Sep
2026 (see below). Every key that touched the old host is considered burned and
was regenerated on the rebuilt host: new operator wallet, new agent keystores,
new RPC tokens. The old ERC-8004 registrations were re-done from the new keys
(
docs/DEVIATIONS.mdD10.5C-05). The compromise post-mortem and the current hardening (fail2ban, ufw default-deny, key-only SSH, immutableld.so.preload, a 20s watchdog service) are indocs/RUNBOOK.md.
One rule, stated three ways because it is the one that would matter to a review:
The LLM never signs and never prices.
- The only model call on any path is category classification (DeepSeek):
given an agent's metadata, which of four categories does it belong to. It runs
in the
classifyworker, under a hard per-run USD cap, and its output is a category label and nothing else. It cannot cause a transaction, set a fee, or change a number a buyer sees as measured. - MCS (the conformance standard) is deterministic arithmetic. It checks facts and compliance with a supplied policy; did the agent's numbers survive recomputation, did it stay inside the constraints it was given. No model is in that loop. Judgement about whether an agent is good goes in the Ledger, which is blind-graded by a human against a rubric registered before the task was run; not by a model.
- Agent responses are parsed as structured data against a schema. They are never interpolated into a prompt that then drives an action, never used to build a query or a command.
- CSP with
frame-ancestors 'none',object-src 'none',upgrade-insecure-requests;connect-srcis an explicit allowlist (self, the named BSC RPC hosts, WalletConnect).'unsafe-eval'has been dropped fromscript-src; nothing in the production bundle needs it, and leaving it in hands an injected string a way to become code.script-src/style-srcstill carry'unsafe-inline', a known Next.js App Router limitation that a nonce would close; not yet done, and tracked rather than glossed. - HSTS,
Cross-Origin-Opener-Policy, a tightenedPermissions-Policy. - Rate limiting on
/api/v1/*and the write endpoints. /api/v1/events(anonymous telemetry) is name-allowlisted, drops any meta key outside a fixed set, caps string and number values, and always returns 204 ; it cannot be used to store arbitrary data or to probe for what exists.- No user accounts, no sessions, no cookies with meaning. The only state a buyer has is on chain.
A review of the server after disk and restart alerts found ordinary internet background noise, not a targeted attack.
What the review found:
- Web traffic. About 39,000 requests in 15 hours, most of them Marque's own health checks and workers. 343 automated probes for
/.env,/.git/config,/wp-adminand PHP files all received 404 or 400; none reached anything. Zero server errors appeared in the alert window. - SSH. 7,439 failed password guesses in the current log. fail2ban had banned 2,943 addresses in total, 106 at the time of the review.
The firewall (ufw, default deny) admits only 22, 80 and 443.
Changes made:
- Scanner probes are refused at the edge. Paths for secrets and other software stacks (
/.env,/.git,*.php,/wp-*and similar) are answered with 404 by Caddy, never rendered by Next.js./.well-knownis untouched. - fail2ban is stricter. It bans for 24 hours after 3 failures, and a
recidivejail bans an address that is banned three times in a day for 7 days, on every port. - SSH allows fewer attempts.
MaxAuthTries 3,LoginGraceTime 20, aMaxStartupsthrottle and no X11 forwarding (/etc/ssh/sshd_config.d/40-marque-hardening.conf). Password login remains for the operator; a key is the recommended next step. - The unused printing service (CUPS, a snap) is stopped and disabled.
- Web workers have more room. The memory cap is 1200M, up from 800M, which ends the hourly restarts that showed up as timeouts and restart-loop alerts.
Security contact: the address in README.md. First-party observations (probe
history, conformance results, run artifacts, seals) are append-only by
policy; there is no code path that deletes one, which also means an attacker
who got write access could not quietly rewrite a track record without it showing
in the anchor chain.
Audited 9 September 2026 (pnpm audit --prod), recorded rather than summarised
away.
Patched. Next.js was pinned at 15.1.3, which predates the August 2026
security release. Production runs 15.5.25 (React 19.2.6) as of 9 September
2026: full test suite green, lint clean, and scripts/verify-candidate.mjs
green on the candidate before cutover and again on production after; 27
route/width combinations with no console error, no CSP refusal and no real
horizontal scroll, plus the wallet modal opening under the tightened
script-src.
The build was verified ahead of the cutover rather than during it.
scripts/build-web.sh with MARQUE_BUILD_DIR builds a candidate into its own
directory, so the running server's static assets are never removed underneath
it; the candidate is then exercised on an alternate port against the live
database and swapped into place by rename. The previous build is kept at
apps/web/.next/standalone-prev, so a rollback is two renames and a restart.
Present and not patched, with the reasoning stated:
drizzle-orm<0.45.2; SQL injection via improperly escaped identifiers (high). Marque is on0.38.x. The advisory concerns identifiers; table and column names interpolated into SQL. Marque never builds an identifier from input: there is nosql.identifiercall anywhere in the tree, and the singlesql.rawcall site (packages/probe/src/worker.ts) interpolates acaseexpression generated from a hard-coded constant array, with no request data on the path. Every other query parameterises its values. The upgrade crosses seven minor versions of the data layer that every worker and page depends on, so it is deliberately deferred past the judging window rather than taken unverified. Exposure is believed nil; that belief is the reason, and it is written down so it can be checked rather than trusted.postcss,ws,uuid,axios(moderate/high). Build-time tooling and transitive wallet-library dependencies, not code paths that serve a request. Tracked for the same post-window upgrade.
The audit JSON behind this section is regenerable with
pnpm audit --prod --json; nothing here is a summary of a summary.