One-Time-View Wallet
Case · TON / crypto infrastructure · 2026
Two separate services - test and combat - generate TON-wallets: 24-word mnemonics, ed25519-pair, address V5R1. The Sid-phrase comes back exactly once in a wallet’s lifetime and never appears again – neither in the API, nor in the panel, nor in the database.
In short: Mnemonics can be collected within 15 minutes, but only seen once. Testnet and mainnet operate as two isolated services, so compromising one does not open access to the other.
The challenge
Internal services need real TON wallets for testing - without going to a real wallet and without the risk of mixing test keys with combat ones. And where there is real money on the other end, you need a wallet with the same logic, but without the right to error: a compromised mnemonic is not a bug, but a direct loss.
The solution is two physically different services on neighboring domains with the same architecture, but without a single shared secret. ton.ifreework.com for testing mainnet.ifreework.com for combat wallets. Compromising one does not give access to the other.
One wallet path
Generation never happens inside a web query – only through a queue, so that a slow external call can’t take the workman and put the site down.
Signed request
Each call carries a HMAC-SHA256 signature on the canonical line (method, path, time, nonce, body hash) and a mandatory
X-Signature · X-Nonce · X-TimestampIdempotency-KeyRepeating the same key returns the already created wallet, and does not produce a second one.Standing in line
202 Accepted · queue: ton-walletsPOSTHe responds instantly, without expecting anything: there is no wallet yet.idstatuspendingAnd the address for the survey.Generation in the Node sidecar
A separate queue worker calls Node service with TON SDK – all cryptography is isolated there because TON SDK is written in JavaScript, not PHP. The sidecar only listens to the internal docker network and requires a shared token.
Node 22 · @ton/crypto · @ton/tonStatus survey
The client is interviewing the same
GET /api/.../{id}iduntil the status becomesreadyorfailed- usually less than a second. A wallet from someone else's request for thisidinaccessible: alienidreturn404, as well as non-existent.Disclosure is all.
Mnemonic is only found in the first successful response to a survey. The next request - at least in a second - will return the same wallet with the
RevealCache · TON_REVEAL_TTLmnemonic: null. Not taken for 15 minutes - the phrase is erased from the temporary cache irrevocably, the address remains, the key is not.
Testnet and mainnet are twins without shared secrets.
Combat service is not a test mode, but a complete clone with a reflashed network: your container, your base, your sidecar, your domain. Nothing is overused.
ton.ifreework.com — testnet
- Global id network
-3He's sewn in the sidecar dead. - Addresses begin with
0Q…/kQ… - Storage mnemonics: by default not stored, optionally encrypted
- Cleaning of overdue records - once every 5 minutes, storage up to a year
mainnet.ifreework.com - combat
- Global id network
-239He's sewn in the sidecar dead. - Addresses begin with
UQ…/EQ… - Storage of mnemonics is prohibited at the configuration level - not just turned off by default
- Clearing expired records – once per minute, storing 180 days
The client on each side also double-checks himself: if the answer suddenly contains an address of the wrong format - UQ… where it should be 0Q…The service refuses to accept it, instead of silently continuing to work with mixed networks.
Incident: how the site was laid five workmen
The real reason is the synchronous call of the Node sidecar inside the php-fpm-worker with a pool of 5 workmen: one slow request, along with a scanner-bot, knocked the site at the same time, took all the workmen at once - and laid down the entire site, including the creation of wallets.
Soon after the first depletion, the creation of wallets began to fall. Logs were silent - but not because everything was fine: directive access_log $ton_log_path … In the configuration, nginx checks for the literal text of the directive, not for what the variable is disclosed in, so nginx substituted before each path. /etc/nginx/ I couldn't write anywhere. Logistics have been silently broken since the deployment and masked the real problem.
The real problem was synchronous sidecar call inside a web request for a default pm.max_children: 5. The fix is not just to increase the pool, but to remove generation from the web query path: it has moved into a separate queue process that cannot be depleted by web traffic, regardless of the size of the php-fpm pool.
What keeps the system on its feet
Sid-phrase not in the logs
The secret object does not store anything in its fields, the individual handler cleans the channel, and the API endpoint does not log the query body.
No dangerous request history
Telescope and Pulse on these services are not installed in principle, and not just turned off in configuration.
Alien ID can not be overcome
ID is verified after HMAC- or session authentication, so an unauthorized response does not disclose the existence of the wallet.
IP cannot be faked
Nginx recovers real address via Cloudflare and docker hops and resets incoming inboxes X-Forwarded-*.
Personalized access
Accounts are created manually by one team, there is no common password, each wallet is fixed created_by.
There is no public API
Caddy, nginx and the application itself independently return 404 for external requests. /api/*.
Architecture
Technology
- Backend — Laravel 13, PHP 8.4
- queue - Redis, separate
queue:workprocess - sidecar — Node 22, @ton/crypto, @ton/ton
- Warehouse MySQL, Wallet Metadata and Audit
Safety and operation
- Authentication HMAC-SHA256, nonce, time window, idempotence
- Network Private channel via 127.0.0.1/VPN
- Panel - username, QR-code, export CSV
- Services Planned cleaning of expired records
The result
Internal services receive one-time test wallets without the risk of hooking combat keys, and where real money is at stake, the same proven mechanism on a completely isolated service: its network, its database, its secrets. Sid-phrases exist in the open form exactly one answer of one query - before and after it simply does not exist.
Read more about the service of creating and binding TON-wallets
Laravel 13 · PHP 8.4 · Node 22 · Redis · MySQL · TON SDK · HMAC-SHA256
