Account Demolisher
Security

Security model

What the deployment can and cannot do, the trust boundaries, the adversaries the design assumes, what each party can see, and the code that defends each boundary.

Closing an account is irreversible. The app keeps your key in your browser or wallet, checks every transaction it asks you to sign against what that step needs, and puts the trust it does ask for behind boundaries you can verify in the code.

What the deployment runs

RouteWhat it does
/api/mediator/sign, /api/mediator/reissueMints an ephemeral mediator for one exchange close, renews its token, and co-signs one strictly validated two-operation forward
/api/plan and its subroutesCollects signatures for a shared-account close and merges them into one envelope, in memory
/api/metricsRecords page visits, funnel steps and verified closes; serves the aggregate behind a bearer token

The deployment does not:

  • receive your secret key, from a wallet or from the pasted-key path;
  • build a transaction for you to sign, or submit one on your behalf. Your browser builds every transaction, your wallet signs it, and your browser submits it. The mediator forward is built and submitted by your browser too; the server only adds the mediator's co-signature.

Position discovery, planning, simulation, the allow-list and authorization checks, and the safety gates all run in the browser. There is no server-side positions proxy.

Server secrets

VariablePower
MEDIATOR_SECRETAn HMAC master that derives every mediator key and signs flow tokens. It never signs a transaction itself and is never an account
METRICS_ADMIN_TOKENReads the aggregate metrics

The mediator and the operator

An honest server co-signs only a forward that pays the destination bound into the flow token, with the memo bound into it, on the network bound into it, with a capped fee and a short time bound. That binding limits what a token that leaks on its own, from browser storage or a log, can be used for. It does not limit:

  • Whoever holds MEDIATOR_SECRET: the operator, or anyone who compromises the server. The holder derives every mediator key, so it can move any balance that sits in a mediator, whatever any token says.
  • A compromised server. The server serves the app code, so it can change the destination and memo the page builds against before you sign.
  • Script running in the page, from a cross-site scripting flaw or a malicious browser extension. It can mint its own flow for a destination and memo of its choosing, so the binding does not constrain it.

An exchange close therefore trusts the operator with the balance while it is in transit. That exposure is bounded by time, not by cryptography: it lasts from the merge into the mediator to the forward out of it, and the two run back to back. A direct close, to an address that needs no memo, never uses the mediator, but it still runs the code the deployment serves, so every close trusts the operator to serve honest code.

Full detail in Mediator forward protocol.

Trust boundaries

BoundaryTrustWhat bounds it
BrowserHighestYour key lives here or in your wallet and nowhere else
WalletSeparate signerShows its own confirmation. The app submits only an envelope whose hash equals the transaction it built and checked, carrying a valid signature from the connected key
ServerTrusted for every closeServes the app code every close runs, so a compromised server can change what the page builds, the destination included, before you sign. That holds for a direct close as much as for an exchange close. Holds MEDIATOR_SECRET, the HMAC master, which only an exchange close relies on: whoever holds it, the operator or anyone who compromises the server, derives every mediator key and can move any balance sitting in a mediator between the merge and the forward. A direct close never uses the mediator, but it still runs the code the deployment serves. The server never receives your key
Horizon and Soroban RPCUntrusted for integrityThey supply the state and simulations a plan is built from. Each classic batch is rebuilt from a fresh audit, each Soroban step is re-simulated, and before every signature the target, the authorization tree and the fee are checked, so a lying endpoint cannot widen what you sign
LumeusUntrustedA hosted Stellar history index that only adds candidates to the allowance, held-token and Aquarius scans. Every allowance is read back with allowance(), every token with balance(), and every pool through the Aquarius router before the plan uses it. The RPC scans still run, and a Lumeus failure leaves their result and is reported
On-chain contractsUntrustedOnly pinned entry points, plus a router-resolved Aquarius pool for a gauge claim, can be the root of a signed DeFi call, each held to the exact authorization tree it needs. Allowance revokes and token transfers target token contracts found by scanning your account, which anyone can create and send you, so each is held to one exact call shape. Every call is held to a fee cap

Assets

In rough order of severity:

  • Your account and its funds. A close is irreversible, so the destination, the memo and the disposal of every balance are the primary asset.
  • A balance in transit through a mediator. Exposed to whoever holds MEDIATOR_SECRET for the time between the merge and the forward.
  • The mediator HMAC master. Whoever holds it can derive every mediator key.
  • Signing keys. Your key, which never reaches the server, and each flow's mediator key, derived on demand and never stored.
  • Allow-list integrity. If the set of signable contracts could widen at runtime, a malicious position could smuggle an arbitrary call into a signature.
  • Plan correctness. What you sign matches what you reviewed, against current state, and a merge never runs while a position is open.

Adversaries and what stops them

AdversaryMitigation
A hostile token contract emitting fabricated events, or adding calls to a simulated authorization treeA revoke is held to exactly approve(user, spender, 0, ledger) and a token transfer to exactly transfer(user, destination, amount), each with source-account credentials and no sub-invocations. Every DeFi step is held to the exact tree its call needs. Untrusted-token steps carry a fee and write cap. A token reporting more than 20 spenders is cut and flagged as hostile
A compromised or hostile RPC or Horizon returning wrong balances, stale state or a manipulated simulationFresh audit and rebuild before each classic batch, re-simulation of each Soroban step, the sign-time target, tree and fee checks, and a merge guard that refuses while any DeFi position is open or a protocol could not be read. A failed fee read fails the step instead of bidding the minimum
A hostile wallet or extension returning a different envelopeverifySignedEnvelope refuses any envelope whose hash differs from the transaction the app built, or that carries no valid signature from the connected key
A network attacker in the middleHTTPS from the TLS-terminating proxy, HSTS in production, and a CSP that lists every host the browser is allowed to contact
A malicious co-signer trying to get a different transaction signed, or to bloat the requestSignatures merge only when the transaction hash matches the stored one and the signature verifies against one of the account's signers. Deduplication keys on the recovered signer, the envelope stops at the threshold, and the app checks the bundle against the live account before submitting
Someone holding a flow token leaked from storage or a logThe co-signer signs only a forward to the committed destination, with the committed memo, on the committed network, with a fee of at most 2,000,000 stroops and no preconditions beyond a short time bound
A manipulated marketA conversion whose full-size price is more than 2% worse than a reference trade of 1/1000 of the balance is never sold without your per-token decision. Minimum outputs carry the configured slippage (1% by default), and a fresh quote worse than the reviewed one stops the close
A supply-chain attackerA committed lockfile installed with --frozen-lockfile, a dependency audit in CI, a digest-pinned base image, and a CSP that loads no third-party script origin
Abuse of the API routesEvery route is rate-limited per client address, request bodies are capped (and capped again at the proxy), and the relay accepts only a close already signed by one of the account's signers

Safety gates before signing

  • Typed confirmation. You type the last four characters of the destination, compared without regard to case, and a four-second timer has to run out before the confirm button enables. The same screen shows the memo. This catches a wrong paste and a swapped clipboard.
  • High-value acknowledgement. Required when the close moves more than 1,000 XLM, counting native XLM, quoted conversions of credit balances, and XLM claimed from claimable balances, or when it moves any holding the app cannot price.
  • Scam-token checks. Four checks run over the account's assets and standalone tokens:
    • Symbol collision: a well-known symbol (XLM, USDC, EURC, AQUA, BLND) from an issuer or contract other than its canonical one, where the network has a canonical issuer for that symbol, and any credit asset coded XLM. On testnet, AQUA and BLND have no canonical issuer, so the check does not flag them there.
    • Look-alike symbol: within an edit distance of 2 of one of those symbols.
    • Suspicious characters: a symbol with characters outside A to Z, a to z and 0 to 9, or a token name with hidden or text-reordering characters.
    • Unrecognized contract: a token contract that is not on this network's curated list and is not the asset contract of one of the well-known assets.
  • Memo enforcement. A destination in the exchange registry, or one that publishes the SEP-29 memo flag, needs a well-formed memo of an accepted type before the close can start. Exchange internal wallets are refused outright.
  • Mainnet notice. Whenever the app is on mainnet (the build default, a restored selection or a switch), the browser must have acknowledged that the code has not had an independent audit. The acknowledgement is stored in local storage. The notice follows the network the app has selected, not the one a signing request names: a co-signer on /sign sees it whenever their browser is on mainnet without an acknowledgement.

Contract allow-list

Every DeFi-protocol Soroban transaction is checked against a compiled-in, network-specific list of entry points before it is signed, together with its authorization tree and its fee. Contract creation and wasm upload are always refused. See Contract allow-list.

What the app and its providers can see

The browser talks directly to these hosts, and to nothing else outside the deployment:

HostWhenWhat it sees
Horizon (horizon.stellar.org, horizon-testnet.stellar.org)Every audit, read and classic submissionYour address, the accounts you read, every transaction you submit
Soroban RPC (the primary for the network and its failovers)Discovery, simulation and Soroban submissionYour address and every Soroban transaction
friendbot.stellar.orgCreating a testnet demo accountThe new testnet address
amm-api.aqua.network, amm-api-testnet.aqua.networkAquarius pool discoveryYour address
Lumeus (api.lumeus.dev, assets.lumeus.dev, api-testnet.lumeus.dev, assets-testnet.lumeus.dev)Discovery and the allowance viewer, only when the build has a Lumeus key for the networkYour address. The browser sends it with a publishable key that is read-only and answers only from the deployment's origin
WalletConnect and Reown hosts (relay, RPC, API and verify)Only when you pick WalletConnect, sign with a restored WalletConnect session, or disconnect or close a restored sessionThe relay carries the encrypted session. AppKit analytics and sign-client telemetry are off. AppKit's mandatory initialization and connection events, which carry the page URL, go to pulse.walletconnect.org, which connect-src leaves out, so the browser blocks them
Your wallet's own servicesWhatever the wallet you pick doesOutside the app's control

Usage metrics

The app sends first-party usage events to its own /api/metrics, never to a third party:

EventSent whenCarries
visitThe landing, /demolish, /allowances or /sign page loadsThe page, and the network where it applies
funnelYou connect a wallet, discovery completes, a plan is built, or a close startsThe step and the network
closeA close completesThe network and the merge transaction's hash

No address, wallet identifier or amount is sent, but the merge hash identifies the closed account on the public ledger. The server stores visits as daily counts per page and network, funnel steps as counts per network and step, and a close only after verifyCloseTx (src/server/close-verify.ts) confirms on Horizon that the hash is a successful merge, shaped like a close this app builds, that carries the on-chain close marker: a manageData set and delete of account-demolisher.close right before the merge. The marker is public, so anyone can copy it into their own merge; the count means merges that carried it, and per-network daily caps (1,000 on mainnet, 200 on testnet) bound what copying adds. The store keeps the verified closes in closes.jsonl and the counts in counters.json under METRICS_DB_DIR.

GET /api/metrics answers 503 until METRICS_ADMIN_TOKEN is set to at least 16 characters, and 401 without the matching Authorization: Bearer token.

Content Security Policy

Built in src/lib/config/csp.ts and set on every document response by src/proxy.ts (Next 16's name for middleware). /api is outside the proxy's matcher, so route bodies are not buffered before their caps apply. next.config.ts sets the static headers on every response: X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, a Permissions-Policy that denies camera, microphone and geolocation, and HSTS in production.

DirectiveProduction value
default-src'self'
connect-src'self', Horizon, Soroban RPC and its failovers and friendbot (derived from NETWORKS), the Aquarius API and the Lumeus Activity and Assets hosts on both networks, and the WalletConnect and Reown hosts other than pulse.walletconnect.*
script-src'self' 'unsafe-inline' 'wasm-unsafe-eval'
style-src'self' 'unsafe-inline'
img-src'self' data: and the wallet-icon hosts
font-src'self' and fonts.reown.com
frame-src'self' and the WalletConnect verify hosts
object-src'none'
frame-ancestors'none'
base-uri, form-action'self'

connect-src is derived from NETWORKS in src/lib/config/networks.ts, so adding a network endpoint there adds it to the policy.

script-src allows 'unsafe-inline' because Next's inline hydration scripts are not nonce-stamped in this deployment, and a nonce-only policy blocks hydration. This is a real tradeoff: the policy loads no third-party script origin, but an injected inline script would run, and could reach every origin connect-src allows, including Horizon, which accepts any signed transaction, and the WalletConnect relay. The CSP limits where scripts load from, not where data can go.

Development adds ws: and wss: to connect-src, 'unsafe-eval' to script-src, blob: to img-src, and data: to font-src.

Origin verification

public/stellar.toml is a SEP-1 declaration served at /.well-known/stellar.toml, so embedders can verify the origin. It declares no signing key, because the mediator uses per-flow keys. See SEP support.

On this page