Case · P2P exchange and crypto infrastructure · 2026

QTest P2P

P2P exchange, where users exchange USDT, TON and Jetton tokens for rubles directly with each other, and the platform holds the crypto part in its own custody, compares assets with liabilities every half hour and does not release withdrawal until ledger converges to a penny.

23 377strings of PHP code
64controller
38service
45migration
18artisan-command
34test

The challenge

A P2P exchange is always a trust between two people who will never meet, plus a crypto part that can neither be lost nor doubled. The user enters USDT on TRC-20, TON or Jetton, puts an ad, agrees with the counter in the chat and expects that after confirmation of payment, the platform will release the crypt - without error per penny and without a second write-off when repeated.

At the same time, the exchange itself does not store money in one wallet “just in case”: TON and Jetton come and go through a separate custodial microservice GRAM, and the site must trust its events exactly as much as it can verify them – signature, idempotence and regular reconciliation of residues.

The path of one P2P transaction

  1. Announcement and counteroffer

    The seller publishes the order at its rate - fixed or floating from the exchange; the buyer responds, and a transaction is created for the pair with a price fixed at the time of response.

    Market · Online courses
  2. Reserve and chat deals

    The crypto part of the seller is frozen on the balance sheet immediately so that it cannot be taken into another transaction. The parties agree in chat in real time, attach a check of payment.

    Deal · WebSocket via Reverb
  3. Tamings that don't let a deal hang

    An unpaid transaction is automatically canceled by timeout, unconfirmed after payment escalates, and background jobs monitor this regardless of whether the browser is open to the parties.

    CancelUnpaidDeal / CancelUnconfirmedDeal / AutoRejectDeal
  4. Dispute

    If the parties do not agree, anyone can open a dispute for one of the predefined reasons - the transaction is frozen until the decision of the administrator, who sees the correspondence and the check in its entirety.

    AdminDisputeController Causes from the directory
  5. Defrosting and enrollment

    Confirmation of payment removes the freeze and transfers the crypto part to the buyer with a single atomic operation – an intermediate state in which money “nowhere” does not exist.

    BalanceService · Transactional

Three assets, three different paths to the wallet

USDT on TRON, TON and Jetton tokens do not enter the platform in the same way. TRON transfers have their own logic of energy rental and address monitoring, and TON and Jetton do not touch the blockchain directly from the main site at all – this is the responsibility of the GRAM microservice, which the site calls through a signed HMAC-API.

USDT · TRC-20

Own TRON wallet on mnemonic keys, monitoring incoming transfers and renting energy for outgoing TRC-20 transfer, so as not to pay a full fee to TRX for each output.

TON

Deposits come with confirmed events from GRAM in crypto_depositsIt is separate from the legacy USDT balance. The minimum amount and network (testnet/mainnet) are checked at the reception, not after the fact.

Jetton

Only allowlisted tokens, separate endpoint reception and immutable jetton_ledger_entries. Withdrawal reserves the amount and commission immediately, and deducts - only after confirmation from GRAM.

What keeps the system on its feet

  • Re-delivery does not upset the balance Each event from GRAM carries Idempotency-Key and external_idRepeated call returns the same result credited=falseIt doesn't create a second operation.
  • Assets and liabilities are verified by themselvesgram:custody:audit Every minute 30 compares managed TON-wallets GRAM with the amount of obligations to users and holds the admission to 0.1 TON on network commissions - the discrepancy over the tolerance goes to the administrator at allert.
  • API GRAM closed on all sides - HMAC signature, time stamp, disposable nonce, allowlist IP and mandatory HTTPS - together, not separately. The client for accepting deposits does not match the client that the site itself calls GRAM.
  • Testnet and mainnet do not physically overlap The incoming event network is checked against the configured network at the reception; a separate command securely resets the testnet account without touching any real operations.
  • Crypt output – under manual control, not by button Withdrawal requests for USDT and Jetton go to the administrator for verification; for Jetton, the reserve is withdrawn atomically and released back if the application is rejected or failed.
  • Account Login and Output – Different Thresholds Google 2FA at the entrance to sensitive actions and a separate PIN at the confirmation of the withdrawal: the compromise of the password itself does not give access to the withdrawal of funds.

Architecture

Technology
Beckend — Laravel 12, MySQL, Redis, queues
frontend — Inertia.js, Vue 3, Tailwind
Realtai n — Laravel Reverb (WebSocket)
TRON Own TRC-20 USDT wallet, energy rental
TON / Jetton Custodial microservice GRAM, HMAC-API
Security and integration
Security Google 2FA, PIN on output, IP allowlist, idempotence
Integration Telegram bot and mini app, Web Push, PDF access recovery
Infrastructure — Docker: nginx, app, queue, scheduler, reverb, db, redis

The result

The user simply exchanges the crypt for rubles with another person in the chat – and under the hood, each penny goes through the freeze, the idempotent ledger and the planned reconciliation of assets, so that neither a double deposit nor a suspended transaction go unnoticed.

← About this platform on the service page

Laravel 12 · MySQL · Redis · Laravel Reverb · Vue 3 · Inertia.js · TRC-20 · TON · Jetton · GRAM


Let’s discuss your project

Tell me what you would like to build. I will reply by email.

Or message me on Telegram @ifwcom