Account Demolisher
Security

Security model

What the deployment can and cannot do, the trust boundaries, the adversaries the design assumes, and the code that defends each one.

Closing an account is irreversible. The design keeps the trust the app asks of you as small as it can, and puts the remaining trust behind narrow, verifiable boundaries.

What the deployment can do

The server side has three jobs:

RouteWhat it does
/api/mediator/signMints an ephemeral mediator for one closure and co-signs a strictly validated two-operation forward with that flow's own key
/api/plan and its subroutesCollects signatures for a multisig close and merges them into one canonical envelope, in memory
/api/soroswapProxies the Soroswap aggregator, concealing the upstream API key, allowing only a fixed set of aggregator operations, rate limited to 5 requests per minute per IP

The deployment does not:

  • Hold your secret key. It never reaches the server.
  • Build transactions on your behalf without you reviewing them first.
  • Submit transactions on your behalf. Your wallet signs and your browser submits.

Position discovery runs entirely client-side. There is no server-side positions proxy and no positions API key.

The mediator key

The only privileged secret is MEDIATOR_SECRET, and it is not stored as a ready-to-use signing key. It is a Stellar seed used purely as an HMAC master key. A fresh ephemeral keypair is derived from it per closure, and the master itself signs nothing and holds no balance.

Its power is bounded three ways: a validator that accepts exactly one envelope shape, both operations bound to the destination committed in the flow token, and a 15-minute token lifetime.

Full detail in Mediator forward protocol.

Trust boundaries

BoundaryTrustWhy
Client browserHighestYour key lives here and nowhere else. Discovery, planning, simulation, the allow-list check, the safety gates, and signing all happen here
ServerBoundedHolds the HMAC master but never your key. A fully compromised server still cannot move funds anywhere except the destination you committed to
Horizon and Soroban RPCUntrusted for integrityRead results are treated as hints. The classic close is re-audited and rebuilt from fresh state at submit, Soroban nodes are re-simulated, and event-derived allowances are confirmed against on-chain reads
WalletTrusted signer, separate domainShows its own confirmation and signs on its own. The app hands it an XDR and gets an envelope back
On-chain contractsUntrustedAny contract a position points at is untrusted until it is allow-listed, and even an allow-listed call is re-checked immediately before signing

Assets

In rough order of severity:

  • Your account and its funds. A close is irreversible, so getting the destination and the disposal right is the primary asset.
  • The mediator HMAC master. A server-only seed. It never signs directly and never holds a balance.
  • Signing keys. Your wallet key, which never reaches the server, and each flow's ephemeral 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. That what you sign matches the plan you reviewed against current state, and that a merge never runs while a position is open.

Adversaries and what stops them

AdversaryMitigation
A malicious dApp or token contract emitting fabricated approve or transfer events, or returning a hostile authorization tree from simulationA revoke is held to an exact zero-approval with no smuggled auth, source-account credentials only, and no sub-invocations. A token drain is held to an exact transfer the same way. A fabricated approve event is caught by confirming the allowance against the token's own on-chain allowance()
A compromised or hostile RPC or Horizon returning wrong balances, stale footprints, or a manipulated simulationReads are re-verified rather than trusted: fresh re-audit and rebuild at submit, re-simulation of footprints, and a fail-closed merge guard that refuses when positions cannot be confirmed closed. Transient 5xx reads retry with backoff while a 404 is fatal
A network attacker in the middleA strict CSP restricts connect-src to a fixed upstream list, with object-src 'none', frame-ancestors 'none', and form-action 'self'. HTTPS is enforced by the TLS-terminating proxy
A malicious co-signer trying to get a different transaction signed, or to bloat the requestSignatures merge only when the partial's transaction hash equals the canonical hash and the signature verifies against an expected signer. Deduplication keys on the recovered public key, not the attacker-chosen hint
A hostile exchange or a redirected forwardThe mediator co-signs one envelope shape only, with both operations bound to the destination committed in the flow token
A supply-chain attackerA committed lockfile, no third-party analytics or telemetry SDKs in the dependency graph, and a CSP that loads no external script origin
The operatorThe server never receives your key and cannot build or submit on your behalf. API routes are rate limited per client IP, and the relay requires a transaction already signed by an authorized on-chain signer

Safety gates before signing

  • Typed confirmation. You type the last four characters of the destination, and a four-second timer must elapse, before the confirm button enables. This catches a wrong paste and a hijacked clipboard.
  • High-value acknowledgement above 1000 XLM.
  • Scam-token heuristics flagging symbol collisions with well-known assets from non-canonical issuers, look-alike symbols within a small edit distance, and symbols containing characters outside A-Z and 0-9.
  • Memo enforcement for known exchanges, refusing to start on a missing, wrong-typed, or malformed memo.

Contract allow-list

Every Soroban transaction is checked against a compiled-in, network-specific contract set immediately before signing, and contract-creation and WASM-upload host functions are always rejected.

See Contract allow-list.

Content Security Policy

Built in src/lib/config/csp.ts and applied per response in src/proxy.ts, which is Next 16's renamed middleware convention. next.config.ts sets only the static headers such as HSTS.

connect-src allows 'self' plus a fixed list: Horizon on all three networks, Soroban RPC on all three plus the pinned failover hosts, the Aquarius API on mainnet and testnet, the Soroswap API, friendbot on testnet and futurenet, and the WalletConnect and Reown hosts.

The WalletConnect hosts are always in the policy; the browser contacts them only when you connect a WalletConnect wallet.

script-src is 'self' 'unsafe-inline' 'wasm-unsafe-eval'. The inline allowance is there because Next's hydration scripts are not nonce-stamped in this deployment and a nonce-only policy would block hydration entirely, as well as forcing every page out of static prerendering. No third-party script origin is allowed, and default-src, object-src, frame-ancestors, and base-uri are all locked down, which bounds the residual risk.

object-src is 'none', framing is denied, and form-action is restricted to 'self'. Any request to an endpoint not on the list is blocked by the browser.

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

To add an upstream, add its URL to CONNECT_SRC_ENDPOINTS.

Origin verification

public/stellar.toml is a SEP-1 declaration served at the canonical /.well-known/stellar.toml path, letting embedders verify the origin. It declares no signing key by design, because the mediator uses per-flow ephemeral keys.

See SEP support.

On this page