VeilSwap
← All posts
Infrastructure2026-11-048 min read

When the Site Is Down, Where Is Your Money?

On 23 October 2026 our host lost power for 22 minutes and 38 seconds. No order was lost, because the deposit address belongs to the liquidity provider, not to us. We also measured what the 503 page really returns, and what the order page can still show while the origin is gone.

On 23 October 2026 the host that runs veilswap.io lost power. The kernel log stops at 08:48:05 UTC and the next boot begins at 09:10:43 — 22 minutes and 38 seconds with no origin server at all. Two facts from that window are worth more than any reassurance. First, zero orders were created between 08:40 and 09:30 UTC, so nobody happened to be mid-payment on our side when the machine died. Second, the database came back with every row it had before: 147 orders today, 119 expired, 21 paid, 5 failed, 2 refunded. The outage was not hidden in a cache. The question people actually ask is smaller and harder: when the page does not load, where is my money, and can I still check this order? This page answers both with measurements.

What the maintenance page actually is. It is a Cloudflare Worker deployed under the name veilswap-maintenance, and it never touches the origin. Its fetch handler has no path logic at all: a page request, an API request and a POST all receive the same 2,393-byte HTML document. Measured on 4 November 2026 at 10:04 UTC: HTTP 503, content-type text/html; charset=utf-8, cache-control no-store, must-revalidate, retry-after 300, plus the marker header x-veilswap-maint: 1. Time to first byte was 48 to 125 milliseconds across five samples, because the answer is produced at the edge and never leaves it. With Accept-Language zh-CN the same page renders 2,396 bytes and lang=zh-CN. A POST returns 503 too — the worker does not read the method either.

Ten seconds to appear, ten seconds to disappear. The switch is the route, not the worker. Binding veilswap.io/__maint-probe/* to the script went live in under ten seconds (404 at t+5s, 503 at t+10s), and deleting it took the same (503 at t+5s, 404 at t+10s). That matters because only half the cycle is automated. A watcher runs every five minutes, checks the origin from inside the host — port 3001 is dropped at the firewall, so an external curl always returns 000 — and deletes the maintenance routes once the app answers 200. Adding them is manual. That is the honest gap, and it is why the honest 503 page is not guaranteed: with nobody posting the route, visitors get Cloudflare's 522, which explains nothing.

In maintenance mode the order page is a skeleton. /order/<id> is a 32,795-byte shell; the status is not in the HTML at all, the browser fetches it from /api/order/<id>. Under the maintenance route that API path returns the same 503 HTML — measured, not assumed, because the worker has no branch for /api. The client logic is written down in the component: a response that is not ok returns early and the page keeps polling every five seconds, and because the order object is still null the view stays on an animated grey placeholder. No error text, no label, no explanation. A JSON parser would throw on that body first. This is a deliberate trade: blocking writes during maintenance is what stops a temporary environment from producing real orders. The cost is equally clear — while the origin is down there is genuinely nothing to query. An offline status view would need a second copy of the database somewhere else, and we do not have one.

Your coins were never on that machine. VeilSwap is non-custodial, and the code makes the phrase literal: the deposit address on your order is the payinAddress returned by the liquidity provider that quoted it. We do not hold a key to it. So when the origin dies, the deposit address keeps working — the provider's own infrastructure is still watching it, and a payment sent during the outage is credited by them, not by us. What we hold is a row: order id, provider order id, amounts, addresses, status. That row is the thing that later turns back into a page. It also explains the only genuinely bad case: losing the order id. Without it the row is hard to find, and the 404 fallback is the one screen in the flow that says your exchange is unaffected.

Three outages, three symptoms. Origin down: 503 from the edge, every path, 48 to 125 milliseconds, with the maintenance copy. Provider API down while the origin lives: /api/order answers 200 carrying the last stored status plus a pollError field, and the order page shows an amber banner saying status sync is delayed — it keeps polling and does not invent a terminal state. Your own network down: nothing resolves, and the 1.11-second quote time measured from this host is not a useful baseline for you. Keeping these apart matters because the advice is opposite. In the first case, wait and keep the link. In the second, do not resend: the deposit may already be credited upstream. In the third, change the path, not the order.

How the status comes back on its own. A cron entry runs every five minutes (vs_sweep.sh, logging ok:true with counters) and re-polls every order that is not terminal, using the provider order id stored at creation. The local clock is 30 minutes from creation; if the provider still says it is waiting for a deposit, the row is marked expired with the local_timeout tag — 87 of today's 147 orders carry that tag. A second pass looks back six hours for locally expired orders that the provider later saw funded, and flips them to paid. Orders the provider cannot find at all are force-expired after 24 hours. The honest note: that six-hour recovery path has produced zero recorded cases so far, so treat it as a mechanism rather than a track record.

Hashes exist only while you are still watching. A completed order returns its status, provider status, amounts, addresses and validUntil — and no transaction hashes. That is measured on a paid order on 4 November 2026, and the database schema has no hash columns either, so once an order reaches a terminal state the hashes are gone from our side; they live in the provider's record and in your wallet. It is also a small contradiction worth flagging rather than hiding: the maintenance page promises that your order link will show the same status and transaction hashes. It will show the status. Copy the payin txid from your wallet when you send, or from the order page while it is still confirming.

Six things to do on an outage day. One: keep the order link, it is the only key. Two: copy the deposit txid from your own wallet before you close it. Three: do not create a second order for the same payment — the original address still works. Four: do not resend because the page is grey; check the chain first (BTC, TRC20, ZEC and LTC all have public explorers; XMR does not, so for monero the provider address activity is the only external check). Five: when the site is back, open the link twice ten minutes apart — a status that moves is a live status. Six: if it stays wrong, email [email protected] with the order link and the txid; that mailbox runs on different infrastructure and is not affected by an origin outage. (Zcash vs Monero 2026)

What we will not promise. We will not promise that the honest 503 page always appears, because posting the route is still manual. We will not promise an offline order lookup, because the database is a single copy on the origin and the 04:00 UTC daily backup exists for recovery, not for serving queries. We will not promise that a page left open survives a reboot, because it polls the same origin that just died. What holds is narrower and checkable: the deposit address belongs to the provider, the order row survives in SQLite with WAL, polling resumes by provider id, and a completed swap is a fact on a chain that neither we nor the provider can edit. When the page and the chain disagree, the chain wins.

Frequently asked questions

Where is my money while the site is down?

It is at the deposit address the liquidity provider issued for your order, and we never held a key to it, so an outage on our side does not touch it. A payment sent during the outage is credited by the provider, and our order row resumes polling it by the provider order id once the origin is back.

Can I check my order status while the maintenance page is up?

No. The maintenance worker has no path logic, so /api/order/<id> gets the same HTML 503 as the homepage (measured on 4 November 2026), and the order page falls back to a grey skeleton that keeps retrying every five seconds. That is deliberate: blocking writes during maintenance keeps a temporary environment from producing real orders.

Why did my order show expired when the payment actually went through?

Because the local clock is 30 minutes from order creation, while the provider keeps its own deadline and may still be waiting when ours passes. The row is marked expired with the local_timeout tag, then a six-hour lookback flips it to paid once the provider reports the funds. Do not resend in that window. Honest caveat: that recovery path has produced zero recorded cases so far, so treat it as a mechanism rather than a track record.

Ready to swap?

No account · No KYC · No email

Exchange now
When the Site Is Down, Where Is Your Money? — VeilSwap