Docs
Threat model
What can go wrong, what stops it, and what is left. Written for the security audit and for
engineers changing the system; docs/TRUST_MODEL.md says the same in plain English for users.
References point to code and to the repository's DECISIONS.md.
What we protect
| Asset | Where it lives |
|---|---|
| Handles' funds (system key) | Each handle's NEAR Intents account; the vault is the only account the MPC network signs its key for |
| Handles' funds (self-custodied) | Same account, controlled only by the owner's key after R7 |
| Public deposits in transit | The handle's derived EVM, Bitcoin and Solana addresses, until swept |
| Privacy | Who received what: confidential balances, private-send recipients, the link between an X account and its activity |
| Sessions | 15-minute agent-signed tokens in an httpOnly cookie |
| Operator funds | Relayer hot wallets (EVM, Solana), the NEAR gas sponsor, the fee treasury |
| The published build | The agent image digest and its code hash, approved on the vault |
Trust boundaries
The vault is the security boundary: it builds or strictly parses everything it signs, fixes every sweep destination, and enforces per-handle, per-token caps and delays on-chain. The agent is trusted to be honest within those limits, and attestation plus governance decide which builds count as the agent.
Threats
Compromised agent (bug or stolen key)
- What it could do: ask the vault to sign anything the vault allows, for any handle still on the system key.
- Mitigations: the vault builds every sweep and intent itself or parses 1Click messages
strictly (
contracts/vault/src/external.rs); sweep destinations are fixed (Omni calldata, or a registered address after a public 24-hour delay and within a daily cap); per-token daily caps and a 24-hour delay above the threshold; owner keys are always queued; the agent's NEAR key lives only in the TEE; the relayer takes an X ID, never an address, and has budgets; governance can trip the 72-hour circuit breaker at once, which stops withdrawals, intents and sweeps. - Residual risk: up to one day's cap per handle and token, across every system-key handle, before the breaker is tripped. Monitoring vault events and the new sweep-address audit (below) shorten that window. Self-custodied handles are out of reach.
Compromised TEE (hardware or firmware flaw)
- Mitigations: registration requires an attested build with approved measurements and an approved CPU (PPID) and expires after 7 days; approving new builds or CPUs waits 14 days; governance can remove an agent instance at once and trip the breaker.
- Residual risk: as for a compromised agent, bounded by the caps. Removing a PPID takes effect when the agent next calls the vault.
X account takeover before the owner signs in
- What it could do: someone who takes over an X account can sign in and act as its owner.
- Mitigations: withdrawals and ownership changes need a sign-in younger than 5 minutes; caps
and the 24-hour delay apply; adding an owner key waits 24 hours in a public queue, and the real
owner (or the agent) can cancel it; the X session token is revoked right after the one
/2/users/mecall. - Residual risk: the attacker can withdraw up to the day's cap. Owners who took full ownership (R7) are not affected. The Trust page says this plainly.
Phishing and lookalike handles
- Mitigations: handles are resolved to numeric X IDs and addresses are derived from the ID,
never from the typed name; lookalike characters, new accounts, protected accounts and recent
renames show warnings on the profile card (
packages/x,apps/web/components/ProfileCard.tsx); the card shows avatar, follower count and creation date before any payment; there is no "return to sender" (R3), so the FAQ says to check the card. - Residual risk: a convincing impersonator account with its own ID. The warnings help; they cannot decide for the sender.
Recycled handles
- Mitigations: everything is keyed by the numeric ID; a handle that now points to a different
ID shows a
handle_recycledwarning, and its addresses are those of the new ID (a new account never sees the old account's funds).
Replayed or expired requests
- Mitigations: web → agent and sweeper → agent RPC is HMAC-signed over method, path, time,
nonce and body hash, within 60 seconds and with a replay cache; every vault request
carries the handle's nonce and an expiry of at most 10 minutes; intents carry versioned NEAR
Intents nonces and deadlines of at most 30 minutes, and each nonce is accepted once; spend
quotes are single-use and expire (
apps/agent/src/intents/spends.ts). - Residual risk: the session revocation list is per agent instance, so a logged-out token stays valid on other instances until it expires (at most 15 minutes).
Sweep destination tampering
- Mitigations: Omni routes write the destination into the calldata inside the vault;
registered routes use the address registered for the handle, which only becomes usable after a
public 24-hour delay and is capped per day; the vault derives Solana token accounts
itself. New in this review: the sweeper, outside the TEE, compares every registered
address of a watched handle with the bridge's own
deposit_addressanswer every hour and raises an error-level alert on any difference (services/sweeper/src/audit.ts); the runbook's response is to trip the circuit breaker within the 24-hour delay.
Relayer drain
- Mitigations: the relayer derives the target address from the X ID itself, so it can only
pay handle addresses; per-funding and per-day budgets per chain; it is not reachable
over the agent RPC; low-balance warnings and
/opsbalances; its keys exist only in the agent. - Residual risk: a compromised agent can spend the daily budget as gas into handle addresses (the gas stays with the handles).
X API credit drain
- Mitigations: per-IP limits on handle lookups (20 per minute), 24-hour positive and 10-minute
negative caches, debounced search, the app-wide 300-per-15-minutes budget watched on
/ops. Fixed in this review: the handle page's metadata looked a handle up before the per-IP limit was checked, so crawlers could skip the limit; the lookup and the limit check are now one per-request step (apps/web/app/(site)/u/[handle]/page.tsx). - Residual risk: many IPs (a botnet) can still spend credits; X's own spending cap is the backstop (HANDOFF).
XSS
- Mitigations: a per-request nonce CSP with
strict-dynamic, no inline scripts, images only from our origin and X's CDN (apps/web/lib/csp.ts); React escapes all output and the code has nodangerouslySetInnerHTMLoreval; X profile text is rendered as text.
CSRF
- Mitigations: every non-GET API route checks
Sec-Fetch-SiteandOriginagainst our host (assertSameOrigin); session cookies arehttpOnly,SameSite=LaxandSecurein production; the ops cookie isSameSite=Strict; OAuthstateis sealed and bound.
SSRF
- Mitigations: no server-side fetch takes a user-supplied URL. The agent and sweeper call fixed hosts from configuration (X API, 1Click, the PoA bridge, the Omni fee API, chain and NEAR RPCs); avatars are loaded by the browser, not the server; open-graph images do no lookups.
Session fixation
- Mitigations: a new session token is minted at every sign-in; tokens are signed by the agent and never accepted from the URL; the pre-login OAuth state cookie is separate and single-use.
Dependency supply chain
- Mitigations: frozen lockfile in CI; install scripts allowed only for an explicit list
(
pnpm-workspace.yamlallowBuilds); exact versions for security-relevant packages; the agent image is digest-pinned and reproducible; the one vendored crate is recorded inVENDORED.md; secret scanning in pre-commit and CI; new: CI fails on high or critical advisories in runtime dependencies (pnpm audit --prod --audit-level high). - Known advisories (not reachable):
elliptic(low) through@near-js/crypto's secp256k1 path, which the agent does not use (its NEAR key is Ed25519);uuidandstream-json(moderate) through@solana/web3.js's RPC client inside@phala/dstack-sdk, which the agent does not call. Recheck at each release.
Logs leaking private data
- Mitigations: the logger replaces sensitive fields by name (tokens, secrets, signatures,
balances, amounts, recipients, senders, deposit and refund fields) and scrubs token-shaped
values from every string, with tests (
packages/logger); money services never log amounts, destinations or balances; X IDs appear only where the data is already public (vault events, sweep-address registrations); the 1Click session token is kept in memory only.
Also considered
- Governance compromise: it can only propose (14-day public timelock) and reduce power at
once (breaker, remove agents, cancel proposals,
lock_forever). Use a multisig (HANDOFF). - MPC network failure: signatures stop; the vault rolls back the day's limits on failed signing; funds do not move.
- 1Click or NEAR Intents failure: sends and spends stop; nothing is signed without a valid quote; public deposits stay on the handle's addresses until sweeps resume.
- Testnet fixture controls: the simulated "pay" buttons exist only in the preview and on testnet fixture mode and move simulated balances only; mainnet refuses fixture mode.
- Passkey-only owners: a passkey works only on this website, so it cannot replace the system key.
Fixed in this review
- Handle lookups in page metadata skipped the per-IP limit (X API credit drain).
- Nothing checked registered sweep addresses against the bridge during their delay: added the hourly audit in the sweeper, with a runbook entry.
- CI had no dependency audit: it now fails on high or critical advisories.
For the security audit
The vault contract (contracts/vault) and the agent's money services
(apps/agent/src/intents, sweeps.ts, relayer.ts) carry the risk. Suggested focus: the strict
parser for 1Click messages, the sweep planners and fee bounds, the cap and delay accounting, the
governance timelock, and the attestation checks in the vendored shade-attestation.