---
title: "3. Exchange Integration"
url: "https://radix.wiki/developers/infrastructure/03-exchange-integration"
updated: 2026-11-14
license: CC-BY-4.0
license_url: "https://creativecommons.org/licenses/by/4.0/"
version: "1.0.0"
---

# 3. Exchange Integration

|  |  |
| --- | --- |
| Series | [Infrastructure track](/developers/infrastructure), part 3 |
| Builder | `SimpleTransactionBuilder` in [@radixdlt/radix-engine-toolkit](https://www.npmjs.com/package/@radixdlt/radix-engine-toolkit) 1.0.6 |
| Node API | Core API `/core/lts/*`, port 3333 |
| Without a node | Gateway API `/stream/transactions` |
| Tested with | Toolkit and Gateway on [Stokenet](/contents/tech/releases/stokenet), 14 November 2026; the Core API was not tested, as no public endpoint answered |

## Introduction

An exchange, custodian or payments backend needs three things from Radix: to send fungible tokens out of accounts it controls, to see which of its users deposited what, and to know its view of the ledger is current. Radix built two interfaces for this, designed to stay compatible across protocol updates: the `/lts` ("long-term support") section of a node's [Core API](/contents/tech/core-protocols/radix-core-api), and the LTS part of the TypeScript [Radix Engine Toolkit](/developers/transactions/04-radix-engine-toolkit), which builds single-signer fungible transfers ([Exchange Integration Guide](https://docs.radixdlt.com/docs/exchange-integration-guide)).

Both assume the integrator runs its own node. This guide follows the official recipe, and gives the [Gateway API](/contents/tech/core-protocols/radix-gateway-api) equivalent of each step for teams reading through a Gateway instead.

## What infrastructure does an exchange need?

The docs recommend two mainnet nodes, a primary and a hot backup to swap to if the primary fails, plus a [Stokenet](/contents/tech/releases/stokenet) node for testing upgrades, which can be shut down between uses ([Infrastructure Setup](https://docs.radixdlt.com/docs/exchange-integration-infrastructure-set-up)). A node is installed with the babylonnode CLI or as a Docker container ([Running a Radix Node](/developers/infrastructure/01-running-a-node)), and serves the Core API on port 3333, at `/core` ([Core API schema](https://github.com/radixdlt/babylon-node/blob/main/core-rust/core-api-server/core-api-schema.yaml)).

Renting the Core API is harder than the docs suggest. The [Radix Foundation](/ecosystem/radix-foundation) never offered a public Core API, and the free Grove endpoint on the docs' list of providers, `radix-mainnet.rpc.grove.city`, did not resolve in DNS on 14 November 2026. NowNodes sells access at `xrd.nownodes.io`, with a month-long free trial ([Core API Providers](https://docs.radixdlt.com/docs/core-api-providers)). The public Gateways at `mainnet.radixdlt.com` and `stokenet.radixdlt.com` answered on the same day, running Gateway release 1.10.6.

## How do I send a transfer?

Before building, the docs list three checks against the node: read the current epoch and `ledger_clock` from `/lts/transaction/construction`, and confirm the clock is close to real time, which shows the node is synced; read the sender's balance from `/lts/state/account-fungible-resource-balance`; and ask `/lts/state/account-deposit-behaviour` whether the recipient's account accepts the resource ([Worked Example 1](https://docs.radixdlt.com/docs/exchange-integration-worked-example-1-transfer-transaction-submission-flow)). Radix accounts can refuse deposits, so the last check is not optional. The Gateway's version of it is `/transaction/account-deposit-pre-validation`, which answers with `allows_try_deposit` per resource and the account rule that decided it.

The toolkit's `SimpleTransactionBuilder` then builds the transaction. This program derives a new account from a random Ed25519 key and sends 100 $XRD; the account was first funded from the Stokenet faucet with the builder's `freeXrdFromFaucet`:

```
import { LTSRadixEngineToolkit, NetworkId, PrivateKey, SimpleTransactionBuilder } from "@radixdlt/radix-engine-toolkit";
import { randomBytes } from "node:crypto";

const GATEWAY = "https://stokenet.radixdlt.com";
const post = (path, body) =>
  fetch(GATEWAY + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
    .then(r => r.json());

// A Node Buffer is rejected: the key must be a plain Uint8Array.
const key = new PrivateKey.Ed25519(new Uint8Array(randomBytes(32)));
const from = await LTSRadixEngineToolkit.Derive.virtualAccountAddress(key.publicKey(), NetworkId.Stokenet);
const xrd = (await LTSRadixEngineToolkit.Derive.knownAddresses(NetworkId.Stokenet)).resources.xrdResource;
const epoch = (await post("/status/gateway-status", {})).ledger_state.epoch;

const intent = (await SimpleTransactionBuilder.new({
  networkId: NetworkId.Stokenet,
  validFromEpoch: epoch,
  fromAccount: from,
  signerPublicKey: key.publicKey(),
}))
  .transferFungible({ toAccount: "account_tdx_2_12x6sn57dkm7530umx5dprkqf7n6mx7pz4lxnt2egcxqxvx4805avlq", resourceAddress: xrd, amount: 100 })
  .compileIntent();

const tx = intent.compileNotarized(key);
await post("/transaction/submit", { notarized_transaction_hex: tx.toHex() });
console.log(tx.transactionId.id); // poll /transaction/status with this until it is no longer Pending
```

Run on 14 November 2026, the faucet transaction and the transfer both reached `CommittedSuccess`. `intent.summarizeTransaction()` reported 5 $XRD of fee locked by the sender, 100 $XRD withdrawn from it and 100 $XRD deposited to the recipient.

The builder's defaults, read from the package source: it locks 5 $XRD for the fee unless `lockedFee()` sets another amount, and any fee not used is returned; it deposits with `try_deposit_or_abort`, so a recipient that refuses the resource fails the transaction instead of the funds disappearing; and the transaction stays valid for 2 epochs, about 5 to 10 minutes, unless `permanentlyRejectAfterEpochs()` changes it. The notary is the sender's key and counts as its signature, so one signature over `hashToNotarize` finishes the transaction; for an HSM, `compileNotarizedAsync` takes a signing callback instead of a key.

The toolkit builds V1 transactions only. A Rust integration can build V2 with [radix-transactions](/developers/transactions/07-rust-transaction-building).

## What do the transaction statuses mean?

Submit the payload to `/lts/transaction/submit` (Gateway: `/transaction/submit`) and poll `/lts/transaction/status` with the transaction ID. Four outcomes need handling ([Worked Example 1](https://docs.radixdlt.com/docs/exchange-integration-worked-example-1-transfer-transaction-submission-flow)):

- **Committed success**: done. The status carries the state version the transaction committed at, which `/lts/stream/transaction-outcomes` turns into its balance changes.
- **Temporary rejection**: the transaction may still commit, for example once the sender holds enough $XRD for the fee. Wait or resubmit the same payload, and do not build a replacement until it has permanently rejected, or both could commit.
- **Permanent rejection**: it can never commit, typically because its last epoch has passed.
- **Committed failure**: it committed, the fee was paid, and nothing else happened.

The last two need a new transaction, which has a new transaction ID.

## How do I tell which user made a deposit?

The docs describe two designs ([Detecting Deposits](https://docs.radixdlt.com/docs/exchange-integration-detecting-deposits-from-different-users)):

- **One account per user.** Generate a key pair for each user, derive its account address with `Derive.virtualAccountAddress`, and store the mapping. The deposit page shows that address and a QR code of `radix:`. Every deposit to the address belongs to that user. The docs call this the better experience, because the user has nothing to type.
- **One shared account and a message.** The deposit page shows one address and a code the user must attach as the transaction message, and the exchange reads the message to credit the right user. A transfer without the code needs manual handling.

In both designs the docs suggest setting the deposit rules of the exchange's accounts to accept only the resources it supports, so a user cannot deposit a token the exchange will not credit ([Deposit and Withdraw Patterns](/developers/scrypto/11-deposit-and-withdraw-patterns) shows the account methods).

## Tracking deposits

Radix has no transfer transaction type: every transaction runs a [manifest](/developers/transactions/01-manifest-language), which may call any component. An integration therefore reads a transaction's **balance changes**, the net change to each account's vaults, rather than its instructions. A transaction in which account X gains 200 $XRD is a 200 $XRD deposit to X, whatever else it did ([Worked Example 3](https://docs.radixdlt.com/docs/exchange-integration-worked-example-3-tracking-deposits-to-specific-account)).

The Core API has two streams. `/lts/stream/transaction-outcomes` returns every committed transaction from a given state version, with its status and fungible balance changes grouped by the global entity that owns each vault; an integrator stores the next state version to read and polls from it ([Worked Example 2](https://docs.radixdlt.com/docs/exchange-integration-worked-example-2-tracking-deposits-to-any-account)). `/lts/stream/account-transaction-outcomes` returns only transactions that touched one account, and suits tracking a few accounts. A transaction can appear in the account stream with no fungible change for that account, for example when it deposited an NFT.

Through a Gateway, `/stream/transactions` does the same with a filter and an opt-in ([Gateway API reference](https://docs.radixdlt.com/api-reference/gateway-api-specs.html)):

```
curl -s -X POST https://stokenet.radixdlt.com/stream/transactions \
  -H 'content-type: application/json' \
  -d '{
    "affected_global_entities_filter": ["account_tdx_2_12x6sn57dkm7530umx5dprkqf7n6mx7pz4lxnt2egcxqxvx4805avlq"],
    "order": "Asc",
    "opt_ins": { "balance_changes": true },
    "limit_per_page": 100
  }'
```

Run on 14 November 2026, this returned two committed transactions for the recipient account above, each with a fungible balance change of +10,000 and +100 $XRD under that account's address. A response with more pages carries a `next_cursor` to pass back; each item's `state_version` is the checkpoint to store.

## Monitoring the node

The `ledger_clock` from `/lts/transaction/construction` shows how far the node's view of the ledger lags real time while the network is live. The node's System API and its Prometheus metrics cover health in more depth ([Worked Example 4](https://docs.radixdlt.com/docs/exchange-integration-worked-example-4-node-status-and-monitoring)). That reading assumes the network is live: during the [network halt of 31 August to 11 September 2026](/developers/infrastructure/01-running-a-node) no node committed anything, so a lag that every node shares points at the network rather than at one node.

## Next Steps

- [Deposit and Withdraw Patterns](/developers/scrypto/11-deposit-and-withdraw-patterns) – deposit rules, authorized depositors and the account locker
- [Building Transactions in Rust](/developers/transactions/07-rust-transaction-building) – V2 transactions and subintents

## External Links

- [Exchange Integration Guide](https://docs.radixdlt.com/docs/exchange-integration-guide) – Radix documentation, with four worked examples
- [LTS toolkit documentation](https://github.com/radixdlt/typescript-radix-engine-toolkit/blob/main/LTS.md) – typescript-radix-engine-toolkit
- [End-to-end Core API example](https://github.com/radixdlt/typescript-radix-engine-toolkit/blob/main/examples/core-e2e-example/main.ts)
- [Core API schema](https://github.com/radixdlt/babylon-node/blob/main/core-rust/core-api-server/core-api-schema.yaml) – the `/lts` endpoints and their models
- [Gateway API reference](https://docs.radixdlt.com/api-reference/gateway-api-specs.html)
