Exchange Tokens with a dApp#
This guide builds a small exchange: a dApp that sells a token for DCC at a fixed price, and buys it back at the same price. A buyer calls the dApp with a DCC payment and receives the token in the same transaction. Nobody has to be online to fill the order.
For trading at market prices between any two tokens, use the DEX instead: its matcher pairs orders and settles them with Exchange transactions.
Note
The script compiles on the public Testnet node (2026-10-25) with complexity 49 (buy) and 32 (sell). The TypeScript is typechecked against @decentralchain/transactions@5.0.1. It has not been broadcast as part of this documentation pass.
Before You Start#
You need:
Node.js 24 or later, with the scripts saved as ES modules.
Two Testnet accounts with DCC from the faucet. The exchange account becomes the dApp and needs a little over 1 DCC: issuing the token costs 1 DCC. The trader account needs the purchase price plus 0.005 DCC for each call.
The
compileRidehelper from Compile a Script, saved ascompile-ride.ts.
npm install @decentralchain/transactions@5.0.1
Write the Script#
Save this as ride/token-exchange.ride:
{-# STDLIB_VERSION 6 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}
# Swaps DCC for one token, and back, at a fixed price. The exchange account
# writes "assetId" (the token ID in base58) and "price" (Decentralites per
# smallest unit of the token) with a Data transaction, and holds a stock of
# both DCC and the token to pay out.
func token() = fromBase58String(getStringValue(this, "assetId"))
func price() = getIntegerValue(this, "price")
@Callable(i)
func buy() = {
if (size(i.payments) != 1) then throw("Attach exactly one DCC payment")
else {
let payment = i.payments[0]
let units = payment.amount / price()
let change = payment.amount % price()
if (isDefined(payment.assetId)) then throw("Pay in DCC")
else if (units == 0) then throw("The payment is less than the price of one token unit")
else if (change == 0) then [ScriptTransfer(i.caller, units, token())]
else [ScriptTransfer(i.caller, units, token()), ScriptTransfer(i.caller, change, unit)]
}
}
@Callable(i)
func sell() = {
if (size(i.payments) != 1) then throw("Attach exactly one token payment")
else {
let payment = i.payments[0]
if (payment.assetId != token()) then throw("Pay in the exchange's token")
else [ScriptTransfer(i.caller, payment.amount * price(), unit)]
}
}
priceis in Decentralites per smallest unit of the token. For a token with 2 decimals, a price of1_000_000makes one whole token cost 1 DCC.buy()takes one DCC payment, sends back as many token units as it pays for, and returns the change.sell()takes one payment in the token and pays out DCC at the same price.Payments reach the dApp before the function runs, so
sell()pays out from DCC that buyers brought in. If the dApp runs out of either asset, the call fails and the caller keeps their payment.
The exchange account keeps its normal signature check, so its owner can still restock, withdraw, or change the price with a Data transaction. Traders have to trust the owner on this. To make the price permanent, add a verifier that returns false, as the pledge drive does.
Deploy the Exchange#
import { readFile } from 'node:fs/promises';
import { issue, data, setScript, invokeScript, broadcast, waitForTx, libs } from '@decentralchain/transactions';
import { compileRide } from './compile-ride.js';
const nodeUrl = 'https://testnet-node.decentralchain.io';
const exchangeSeed = 'exchange account secret seed phrase here';
const traderSeed = 'trader secret seed phrase here';
const exchange = libs.crypto.address(exchangeSeed, '!');
const wait = (id: string) => waitForTx(id, { apiBase: nodeUrl, timeout: 120_000 });
// 1. Issue the token from the exchange account, so the account holds the whole supply
const token = issue(
{
name: 'Coupon',
description: 'Sold at a fixed price by a dApp',
quantity: 1_000_000, // 10,000.00 tokens
decimals: 2,
reissuable: false,
chainId: '!',
},
exchangeSeed,
);
await broadcast(token, nodeUrl);
await wait(token.id);
// 2. Set the token and its price: 1 DCC per whole token is 1,000,000 Decentralites per 0.01 token
const settings = data(
{
data: [
{ key: 'assetId', type: 'string', value: token.id },
{ key: 'price', type: 'integer', value: 1_000_000 },
],
chainId: '!',
},
exchangeSeed,
);
await broadcast(settings, nodeUrl);
await wait(settings.id);
// 3. Compile the script and set it on the exchange account
const source = await readFile(new URL('./ride/token-exchange.ride', import.meta.url), 'utf8');
const deploy = setScript({ script: await compileRide(source, nodeUrl), chainId: '!' }, exchangeSeed);
await broadcast(deploy, nodeUrl);
await wait(deploy.id);
The exchange account issues the token itself, so it holds the whole supply to sell. Each step waits for the previous one to be in a block, for the reason explained in Sending a Transaction That Depends on Another.
Trade#
// 4. The trader buys 5 tokens for 5 DCC...
const buy = invokeScript(
{
dApp: exchange,
call: { function: 'buy', args: [] },
payment: [{ assetId: null, amount: 500_000_000 }],
chainId: '!',
},
traderSeed,
);
await broadcast(buy, nodeUrl);
await wait(buy.id);
// ...and sells 2 of them back for 2 DCC
const sell = invokeScript(
{
dApp: exchange,
call: { function: 'sell', args: [] },
payment: [{ assetId: token.id, amount: 200 }],
chainId: '!',
},
traderSeed,
);
await broadcast(sell, nodeUrl);
const info = await wait(sell.id);
console.log('Sell status:', info.applicationStatus);
The trader pays 5 DCC and gets 500 units, which is 5.00 tokens, then sells 200 units back for 2 DCC. applicationStatus is succeeded when the call went through. A call that fails one of the script’s checks is rejected and costs the trader nothing.