# Watching incoming payments on Solana, Ethereum, Stellar and TON: one task, four designs

> Detecting a deposit looks like the same feature on every chain. It is four different designs: an account subscription, a log subscription with a blind spot, a stream that only says when to look, and a poll.

- Canonical: https://triport.io/blog/watch-incoming-payments-solana-ethereum-stellar-ton
- Published: 2026-09-22
- Updated: 2026-10-05
- Author: Triport Engineering
- Category: Engineering
- Networks: Solana, Ethereum, Stellar, TON

## TL;DR

- There is no portable 'watch this address' primitive: Solana pushes account changes, Ethereum pushes contract logs but not native ETH transfers, Stellar streams transactions you then read payments for, and TON has no push channel, so you poll getTransactions with a logical-time cursor.
- Every design needs the same three parts — a trigger, a stored cursor you advance only after your own commit, and a gap-fill read that runs after every reconnect — because none of these streams replays what you missed.
- On Triport, measured 2026-09-22: /ws/sol and /ws/eth accept WebSocket connections on both regions, Stellar SSE and Horizon payment reads answer, TON getTransactions answers, and there is no TON WebSocket (the route returns 404).

A payments backend that supports four chains usually starts with one abstraction: `watch(address) → onDeposit(amount)`. The abstraction survives about as long as the first chain that does not fit it. This post builds the same feature — tell me when value arrives at an address I control — on Solana, Ethereum, Stellar and TON, and shows where each chain forces a different design. The point is not which chain is easier. It is which assumptions break when you move the same code from one to the next.

## The task, precisely

"Received" has to be defined before any code is written, because each chain answers a slightly different question:

- **What moved?** The chain's native asset, or a token issued by a contract or program.
- **Where does the balance live?** On the address itself, or in a separate account the address owns.
- **What event tells you?** An account-state change, a contract log, a transaction record, or nothing at all until you ask.
- **When is it safe to credit?** Each chain has its own notion of final; our [finality comparison](https://triport.io/blog/finality-commitment-levels-across-chains) maps them side by side.

With that, the four designs.

## Solana: accountSubscribe on the destination

Solana's closest thing to "watch this address" is [`accountSubscribe`](https://triport.io/docs/sol/json-rpc/accountSubscribe) over the JSON-RPC pub/sub WebSocket. You subscribe with a base-58 public key; whenever the runtime commits a change to that account — lamports move, data is rewritten, the owner changes — the server pushes an `accountNotification` with the new state. Between changes the stream is silent. On Triport it runs on `/ws/sol` and is available from the 7-day Free trial up.

Two properties of this subscription decide the design:

1. **It reports state, not transfers.** The reference is explicit that notifications fire once per committed change of state, not once per transaction: several writes in one slot can collapse into a single notification carrying the final state. A balance delta between two notifications can therefore be the sum of several payments, or a payment minus a fee. If you need one record per payment, use the notification as a trigger and fetch the transactions that touched the account.
2. **Tokens live somewhere else.** SOL sits on the wallet's own account, but SPL token balances are held in token accounts. Watching the wallet does not show a USDC deposit; you subscribe to the relevant token account (the reference lists "watching a token account for incoming deposits" as a typical use) and decode it with `jsonParsed`.

A subscription lives only as long as the socket. After a reconnect you subscribe again, and whatever changed in between is not replayed. The gap-fill is a signature list for the account from your last processed signature forward (`getSignaturesForAddress`), followed by a fetch of each transaction. The [wallet-watch tutorial](https://triport.io/docs/guides/watch-a-solana-wallet) builds exactly this loop as runnable code.

## Ethereum: a logs subscription, and why native ETH has no log

On Ethereum the natural push is `eth_subscribe` with the `logs` type, filtered by contract address and topics. ERC-20 defines `event Transfer(address indexed _from, address indexed _to, uint256 _value)`, which "MUST trigger when tokens are transferred"; its first topic is the event signature and, because both addresses are indexed, the third topic is the recipient, so "USDT to my address" becomes a filter on the token contract plus the signature topic plus your address, left-padded to 32 bytes, in the third position. On Triport's [`/ws/eth`](https://triport.io/docs/streaming/eth-ws), `logs` requires the basic tier.

```json
{"jsonrpc":"2.0","id":2,"method":"eth_subscribe","params":["logs",{
  "address":"0xdAC17F958D2ee523a2206206994597C13D831ec7",
  "topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", null, "0x000000000000000000000000<your-address-without-0x>"]
}]}
```

The blind spot is the chain's own asset. Logs exist only when contract code emits them, and moving ETH does not emit one by itself: a wallet-to-wallet transfer produces no log, and a contract can forward ETH without emitting anything. EIP-7708, which would make every ETH transfer emit a log, puts it directly: "Logs work for ERC-20 tokens, but they do not work for ETH." When we read it on 2026-09-22 its status was Review, so no network rule makes ETH transfers visible to a log filter today.

For native ETH you need a second mechanism:

- **Top-level transfers**: follow new blocks and check each transaction's `to` and `value`. `newHeads` gives you the block announcement (pro tier on `/ws/eth`), and `eth_getBlockByNumber` with full transactions gives you the list.
- **Transfers made by contracts** (a smart-contract wallet paying you, a withdrawal from a protocol) never appear as top-level transactions to your address. Only a trace shows them. On Triport the `trace_*` family, such as `trace_block`, starts at the pro tier.

Gap-fill after a reconnect is two reads: `eth_getLogs` over the missed block range for tokens, and a block scan (plus traces if you need them) for ETH.

## Stellar: an SSE trigger plus a paged read

[Stellar](https://triport.io/stellar-rpc) is the only chain here with a Server-Sent Events stream, and it is the clearest example of a stream that tells you *when* to look but not *what* happened. On Triport, Horizon REST and SSE carry the status Limited: they are served through one upstream operator, best-effort, with no dedicated SLA. The Horizon SSE surface mounts exactly two channels, ledgers and transactions, and both must start at `cursor=now`; there is no payments channel and no replay from an older token. So the design is: subscribe to the transactions stream as a trigger, then page your account's payments over Horizon REST from your stored `paging_token`, and advance that cursor only after your own work commits. Check `successful` before crediting, and treat the `410 beyond_retention` answer at the edge of the retention window as "history not kept here", never as "nothing happened". The full implementation, including why the cursor must move after the commit and not before, is in the [guide to monitoring Stellar payments with Horizon SSE](https://triport.io/docs/stellar/guides/monitor-payments-with-horizon-sse).

## TON: no push channel, so poll getTransactions

TON is the chain where the push abstraction simply does not exist on this gateway. Triport's TON surface is a read-only JSON-RPC catalog of 13 methods; the [`getTransactions`](https://triport.io/docs/ton/json-rpc/getTransactions) reference states that no transaction stream or WebSocket subscription is published for TON, and a WebSocket upgrade to `/ws/ton` returned 404 when we checked on 2026-09-22.

`getTransactions` returns an account's transactions newest-first. The cursor is a pair: to continue, you take both `lt` (logical time) and `hash` from the oldest transaction you received and send them together; neither is a cursor on its own. `to_lt` sets a lower logical-time boundary. That shapes a polling loop:

1. Store the `lt` and `hash` of the newest transaction you have fully processed.
2. On each tick, call `getTransactions` for your address without a cursor to get the newest page.
3. If the page does not reach your stored transaction, keep paging older with the `lt`/`hash` of the last item until it does (or pass your stored `lt` as `to_lt`).
4. Process the new transactions oldest-first, then save the newest one as your cursor.

Because TON is sharded, "received" also means "referenced by the masterchain": TON's own payment-processing guide says a transaction becomes irreversible once it appears in a masterchain block, and advises verifying that inclusion rather than trusting the shardchain alone. On Triport, `getTransactions` is on the `ton_read_rpc_heavy` budget, 15 / 20 / 100 / 250 RPS on free / basic / pro / business (default, unmeasured). Polling one address every few seconds is a rounding error against that; polling ten thousand deposit addresses is not, and is the point at which block-level scanning becomes the better design.

## Reconnect and gap-fill on each chain

The four designs share one skeleton: a trigger, a cursor, and a catch-up read that runs every time the trigger restarts.

| | Trigger | Cursor you store | Catch-up read after a gap |
|---|---|---|---|
| Solana | `accountSubscribe` notification | Last processed signature | `getSignaturesForAddress` from that signature, then `getTransaction` |
| Ethereum | `logs` notification (tokens); new block (ETH) | Last processed block number | `eth_getLogs` over the missed range; block scan and traces for ETH |
| Stellar | Transactions SSE frame | `paging_token` of the last processed payment | Horizon account payments from that token, ascending |
| TON | Your own timer | `lt` + `hash` of the last processed transaction | `getTransactions` paging until the stored pair |

Run the catch-up read on startup and after every reconnect, not only after a detected failure. Code that runs constantly stays correct; code that runs once a quarter does not. If a listener goes silent after a reconnect, [why a WebSocket subscription goes silent](https://triport.io/learn/websocket-subscription-reconnect) covers resubscribing and backfilling the gap.

## Comparison table

| | Solana | Ethereum | Stellar | TON |
|---|---|---|---|---|
| Push for this job on Triport | Yes, `/ws/sol` | Yes, `/ws/eth` | Yes, SSE (transactions channel; status Limited) | No |
| What the push carries | New account state | Contract logs | Transaction records | — |
| Native-asset blind spot | None for the account itself | Native ETH emits no log | None (payments are read over REST) | — |
| Token blind spot | Tokens live in token accounts | None for ERC-20 `Transfer` | None | Not covered by this post |
| Replay from the stream | No | No | No (`cursor=now` only) | — |
| Minimum tier for the push | free | basic (`logs`), pro (`newHeads`) | free (`stellar_read_rpc_heavy`) | — |

## When this is the wrong approach

- **Thousands of addresses.** Per-address subscriptions and per-address polling both scale with the address count. At that size you follow blocks (or a filtered stream) and match addresses on your side.
- **You need a full history on day one.** These designs detect new deposits. Reconstructing an address's past is a different job with different limits; see why [an empty address history is not proof of none](https://triport.io/learn/empty-address-history).
- **You credit on the trigger.** None of the triggers here is a finality signal. Credit after your chain-specific confirmation rule, not when the notification arrives.
- **Exactly-once from the stream.** None of these streams replays or deduplicates for you; idempotency keyed on the transaction id is your code's job.

## Sources

1. Triport, [`accountSubscribe`](https://triport.io/docs/sol/json-rpc/accountSubscribe): one account per subscription, state-change notifications that can collapse several writes, resubscribe after reconnect, token-account use case. Streaming overview: `accountSubscribe` free on `/ws/sol`.
2. Triport, [Ethereum Pub/Sub `/ws/eth`](https://triport.io/docs/streaming/eth-ws): `logs` basic, `newHeads` pro, the USDT `Transfer` filter example.
3. EIP-20, token standard (`Transfer` event) — https://eips.ethereum.org/EIPS/eip-20, read 2026-09-22. EIP-7708, "ETH transfers emit a log" — https://eips.ethereum.org/EIPS/eip-7708, read 2026-09-22 (status: Review).
4. Triport, [`trace_block` on Ethereum](https://triport.io/docs/eth/json-rpc/trace_block): pro and business tiers.
5. Triport, [Monitoring Stellar payments with Horizon SSE](https://triport.io/docs/stellar/guides/monitor-payments-with-horizon-sse) and the [Horizon SSE reference](https://triport.io/docs/stellar/streams/sse): two channels, `cursor=now`, `paging_token`, `410 beyond_retention`.
6. Triport, [`getTransactions` on TON](https://triport.io/docs/ton/json-rpc/getTransactions): newest-first, `lt` + `hash` cursor pair, `to_lt`, `ton_read_rpc_heavy` 15/20/100/250 RPS (default, unmeasured), no TON stream or WebSocket.
7. TON Docs, Payment processing overview — https://docs.ton.org/applications/payments/overview, read 2026-09-22.
8. Production checks, 2026-09-22, on the EU and CA origins: `/ws/sol` and `/ws/eth` upgrades succeeded; Stellar SSE and Horizon account payments answered; TON `getTransactions` answered with data; `/ws/ton` returned 404.

## Try it

- Reference: [accountSubscribe on Solana — watch one account over WebSocket](https://triport.io/docs/sol/json-rpc/accountSubscribe)
- Network: [TON RPC — the 13-method read catalog and its limits](https://triport.io/ton-rpc)
