Get the code
npx create-scaffold-hbar@latest --template hedera-dev/hedera-finance-templates#templates/dvp-settlementOverview
In a securities trade, the seller delivers the security and the buyer pays for it. Whichever side moves first is exposed: if the seller delivers and the buyer never pays, the seller has lost the asset. Delivery versus payment (DvP) guarantees that both legs settle together or not at all. On Hedera this needs no smart contract:- A single native
TransferTransactioncan carry the security leg and the payment leg. The network executes it atomically. - Token-level compliance applies to both legs. If the security has a KYC key and the buyer is not granted KYC, the whole transfer fails.
- A scheduled transaction lets the two parties sign at different times, and can hold a fully signed trade until a settlement time.
- A Hedera Consensus Service (HCS) topic records every event in a trade’s life, in order and tamper-evident.
- Issuer: the operator account the app runs with. Creates the security and the stablecoin, grants KYC, funds the other parties, and publishes the audit log. It pays every transaction fee.
- Seller: holds the security, proposes trades, signs first, and can cancel a trade before it settles.
- Buyer: holds the stablecoin and signs second. The template creates two buyers: one with KYC on the security and one without, so you can watch a trade fail safely.
Prerequisites
- Node.js 20.18 or later, and Yarn
- A Hedera testnet account from the Hedera Portal. It becomes the Issuer. Its key can be a raw ECDSA key in hex, or a DER-encoded ECDSA or ED25519 key.
- Testnet HBAR on that account. Setting up a market costs about 31 HBAR, mostly the network fee for creating two tokens; each trade and its audit entries cost well under 1 HBAR.
- Optional, for Trade with HashPack: the HashPack browser extension with a testnet account other than the Issuer (two accounts to trade wallet to wallet), and a WalletConnect project id from the Reown dashboard
Scheduled transactions are native Hedera transactions, so they must be signed by a Hedera wallet such as HashPack. MetaMask cannot sign them.
Project structure
Configuration
Createpackages/nextjs/.env.local:
Set up the market
Press Set up market. The server runs seven steps as the Issuer and reports each as it starts and finishes, so the page shows a checklist: a spinner on the running step, then a check with the ids it created. What each step does expands to a one-line explanation per step.Step 1: Create the security and the stablecoin
The security has 0 decimals and a KYC key, so the network refuses to move it into or out of any account the Issuer has not approved. It also has a freeze key, which a regulator-driven workflow could use to halt an account. The stablecoin has 6 decimals and no KYC key, like USDC.Step 2: Associate the tokens and grant KYC
A Hedera account must opt in to a token before it can hold it (token association). Each demo account associates both tokens, signing with its own key while the Issuer pays the fee. The Issuer then grants KYC on the security to the Seller and the Buyer, but not to the unverified Buyer.Step 3: Fund the parties and create the audit topic
One transfer gives the Seller 200 security, the Buyer 20,000 stablecoin and the unverified Buyer 10,000 stablecoin. Under the market table the page states this starting position in words, so what is left after a trade is never mistaken for newly created tokens. The last step creates the audit topic. Its submit key is the Issuer’s, so only the Issuer can write to it; anyone can read it. See The audit log. You should see the balances in the market table, read from the mirror node, and the audit topic id.Settle a trade
Step 1: The Seller proposes the trade
Fill in the ticket: quantity, price and buyer, and leave Settlement on On the Buyer’s signature. Both legs go into oneTransferTransaction. Calling .schedule() wraps it in a ScheduleCreateTransaction, which the Seller signs. Every schedule also gets:
- an admin key: the Seller’s, so only the Seller can cancel it;
- an expiration time: 30 minutes for a trade that settles on signature. Unsigned by then, it expires and nothing moves.
PENDING, its schedule id linked to HashScan, and when it expires. No balance changes: one signature is not enough to move anything. The audit log gains a proposed entry.
Step 2: The Buyer signs
Press the row’s Sign as Buyer. The Buyer adds its signature with aScheduleSignTransaction; as soon as the schedule has every signature it needs, the network executes the transfer.
SETTLED with result SUCCESS, and the changes per party: with the default ticket of 100 security for 10,000 stablecoin, Seller: −100 security, +10,000 stablecoin and Buyer: +100 security, −10,000 stablecoin. The audit log gains signed and settled entries.
Step 3: Verify settlement
Open the schedule on HashScan and follow it to its executed transaction. It is one transaction with one consensus timestamp, carrying four token transfers: both sides of the security leg and both sides of the payment leg. You can read the same thing from the mirror node:Settle at a set time
Real markets settle on a date, such as T+1: the trade is agreed today and settles tomorrow. Choose At a set time in the ticket and a delay in minutes (1 to 60, default 2). The template uses minutes so you can watch it happen; the mechanism is the same for a day. The schedule’s expiration time becomes the settlement time, andwaitForExpiry tells the network to hold the transfer until then even once every signature is in:
SCHEDULED and nothing moves yet. At the set time the network executes it by itself. No one presses anything, so the server watches the schedule on the mirror node (executed_timestamp, deleted, expiration_time) every few seconds and updates the trade, the balances and the audit log when the execution appears.
You should see SCHEDULED after the Buyer signs, then, shortly after the set time and without touching the page, SETTLED with the new balances and a settled entry.
Cancel or let a trade expire
Cancel. Until a trade executes, the Seller can press its Cancel. Because the schedule’s admin key is the Seller’s, the Seller signs aScheduleDeleteTransaction:
CANCELLED, can no longer be signed, nothing moves, and a cancelled entry is logged.
Expiry. A trade the Buyer never signs becomes EXPIRED at its deadline: after 30 minutes for a trade on signature, or at the set time. Nothing moves, and an expired entry is logged.
A
waitForExpiry trade that expires unsigned is EXPIRED. The network tries the transfer and it fails with INVALID_SIGNATURE, but the Buyer never agreed, so this is an expiry, not a rejected trade.What happens when a leg fails
Select the unverified Buyer, propose a trade, and sign as that Buyer. Both parties sign, the schedule executes, and the network rejects the transfer withACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN.
You should see status REJECTED, the row saying that nothing moved, no balance changes, and a rejected entry. The unverified Buyer still holds all of its stablecoin, even though only the security leg failed the KYC check. That is the guarantee: a DvP transfer can fail, but it can never half-settle.
Other ways a trade fails, with the same outcome:
- Frozen account: the Issuer froze either party on the security.
- Insufficient balance: the Seller spent the security, or the Buyer the stablecoin, between proposing and settling.
The audit log
Every event in a trade’s life is published to the market’s HCS topic:proposed, signed, settled, rejected, cancelled and expired. Each entry is JSON with the schedule, the terms, the two parties, the transaction it is about and its result, and prev: the SHA-256 of the previous entry’s exact bytes (64 zeros for the first). Change or remove any entry and every later prev stops matching.
Trade with HashPack
WithNEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID set, switch Sign with to HashPack to connect and onboard. Demo accounts stay available.
- Connect HashPack. Use your own testnet account. The app refuses the Issuer, which created both tokens and holds their treasury, and the demo accounts.
- Choose a role (Seller or Buyer) and press Onboard wallet. HashPack signs the token association, because only the account owner can opt in to a token. The server then grants KYC and funds your account for its role, minting the tokens it hands you.
- Trade. Buyer in the ticket is always the demo buyer. After a buyer wallet is onboarded, the dropdown adds My wallet (0.0.xxx). Unverified Buyer stays the buyer without KYC. Propose as demo Seller is always there. Once a seller wallet is onboarded, Propose as my wallet sits beside it, and HashPack signs that proposal and any cancellation of a trade that wallet proposed. When the trade’s buyer is your wallet, HashPack signs the schedule. Settling at a set time works the same way.
- One wallet can take both roles. Onboarding in the second role funds it for that role without asking HashPack to associate the tokens again; onboarding twice in the same role is refused.
- Nothing can be onboarded while a trade is pending or scheduled.
- Keep HashPack on the account you onboarded. Wallet transactions are built with that account as the payer. If you switch accounts, Propose as my wallet and signing or cancelling a wallet trade stay disabled until you switch back. The page names the account. Proposing and signing as a demo party stay available.
- The server never trusts what the browser sends back after HashPack signs. It records the trade terms it prepared itself, checks the transaction was submitted by the onboarded wallet, and checks the wallet’s own transaction succeeded.
Going to production
- Real stablecoin: replace the app’s stablecoin with USDC (
0.0.456858on mainnet). - Real KYC: grant KYC from your identity provider’s decision rather than at setup.
- Maker-checker for the Issuer: hold the KYC, freeze and supply keys in a threshold key, so admin actions need more than one approver.
- Custody: sign with a custody provider or HSM rather than a key in an environment variable.
- No demo accounts: in production, every party signs with its own wallet or custody; the server never holds a party’s key.
- Durable state: the template keeps the market in server memory. Persist trades, or rebuild them from the schedules and the audit log on the mirror node, so a restart loses nothing.
- Real settlement dates: use a day-scale delay (up to 62 days) for T+1 or T+2, and a mirror node subscription rather than polling to notice settlements.
Next steps
Scheduled transactions
How schedules collect signatures, wait for expiry, and execute.
Atomic swaps
Several transfers in one transaction, the core of DvP.
KYC on HTS tokens
Granting and revoking KYC on a token.
Scaffold HBAR
The CLI and the other templates.