How to Build a Bonding Curve DEX with Clarity on Stacks

Table of contents
Introduction
Welcome to Part 2 of our STX.CITY Mini tutorial series! In the previous tutorial, we built a complete SIP-010 token contract. Now we'll create a sophisticated bonding curve DEX (Decentralized Exchange) that enables automatic token trading with dynamic pricing.
By the end of this tutorial, you'll have a fully functional DEX that allows users to buy and sell tokens using a constant product bonding curve, complete with fee distribution and liquidity management.
What You'll Learn
Understanding bonding curve mechanics and automated market makers
Implementing constant product formula (x × y = k)
Building secure buy/sell functions with fee distribution
Creating initialization and state management systems
Testing DEX functionality with real trading scenarios
Prerequisites
Completed Part 1 (SIP-010 Token Contract)
Understanding of basic AMM concepts
Familiarity with Clarity programming from Part 1
Understanding Bonding Curves
What is a Bonding Curve?
A bonding curve is an automated market maker that determines token price based on supply and demand through a mathematical formula. As more tokens are bought, the price increases; as tokens are sold, the price decreases.
Our Implementation: Constant Product Formula
We'll use the constant product formula: x × y = k
Where:
x = STX liquidity in the pool
y = Token supply available for trading
k = Constant product (invariant)
Key Benefits
No Upfront Liquidity: Tokens can be traded immediately after deployment
Automatic Price Discovery: Price adjusts based on trading activity
Always Available Liquidity: Users can always buy or sell tokens
Fee Generation: Trading fees provide revenue to the platform and the token creator
Setting Up the DEX Contract
Let's build our DEX contract step by step.
Create a new contract we’ll call dex here:
clarinet contract new dex
1. Contract Header and Constants
;; @title Bonding Curve DEX for STX.CITY Mini Version
;; @version 1.0
;; @summary A decentralized exchange facilitating token trading using bonding curve mechanism
;; @description This DEX allows users to buy and sell tokens through a bonding curve, with automatic liquidity provision
;; Import SIP-010 trait
(use-trait sip-010-trait 'STF0V8KWBS70F0WDKTMY65B3G591NN52PR4Z71Y3.sip-010-trait-ft-standard.sip-010-trait)
;; Error constants
(define-constant ERR-UNAUTHORIZED (err u401))
(define-constant ERR-INVALID-AMOUNT (err u402))
(define-constant ERR-INSUFFICIENT-BALANCE (err u403))
(define-constant ERR-SLIPPAGE-TOO-HIGH (err u404))
(define-constant ERR-TRADING-DISABLED (err u1001))
(define-constant ERR-ALREADY-INITIALIZED (err u1002))
(define-constant ERR-NOT-INITIALIZED (err u1003))
What this does:
Imports the SIP-010 trait so we can interact with any compliant token
Defines specific error codes for different failure scenarios
Sets up the foundation for secure DEX operations
2. DEX Configuration Constants
;; DEX Configuration
(define-constant TARGET-STX u3000000000) ;; 3000 STX target for graduation
(define-constant VIRTUAL-STX u600000000) ;; 600 STX virtual liquidity
(define-constant FEE-PERCENTAGE u2) ;; 2% trading fee
(define-constant PLATFORM-FEE-ADDRESS 'ST1WTA0YBPC5R6GDMPPJCEDEA6Z2ZEPNMQ4C39W6M)
What this does:
TARGET-STX: When reached, token "graduates" to a full liquidity pool
VIRTUAL-STX: Initial virtual liquidity for price calculations
FEE-PERCENTAGE: Trading fee split between platform and token creator
PLATFORM-FEE-ADDRESS: Where platform fees are sent
3. State Variables
;; State variables
(define-data-var associated-token (optional principal) none)
(define-data-var initialized bool false)
(define-data-var tradable bool false)
(define-data-var virtual-stx-amount uint VIRTUAL-STX)
What this does:
associated-token: Which token this DEX trades
initialized: Whether the DEX has been set up
tradable: Whether trading is currently enabled
virtual-stx-amount: Current virtual STX for price calculations
Core DEX Functions
1. Initialization Function
;; Initialize the DEX with a token contract
(define-public (initialize (token-contract principal) (initial-token-amount uint) (initial-stx-amount uint))
(begin
;; Check if already initialized
(asserts! (not (var-get initialized)) ERR-ALREADY-INITIALIZED)
;; Set the associated token
(var-set associated-token (some token-contract))
;; Mark as initialized and tradable
(var-set initialized true)
(var-set tradable true)
;; Set virtual STX amount
(var-set virtual-stx-amount initial-stx-amount)
(ok true)
)
)
What this does:
Links the DEX to a specific token contract
Prevents double initialization
Enables trading functionality
Sets up initial virtual liquidity parameters
2. Buy Function - Converting STX to Tokens
;; Buy tokens with STX using bonding curve
(define-public (buy (token-trait <sip-010-trait>) (stx-amount uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (unwrap-panic (contract-call? token-trait get-balance (as-contract tx-sender))))
(virtual-stx (var-get virtual-stx-amount))
;; Calculate tokens to receive using constant product formula
;; tokens_out = (token_reserve * stx_in) / (stx_reserve + stx_in)
(tokens-out (/ (* current-token-balance stx-amount) (+ current-stx-balance stx-amount)))
;; Calculate fees (2% of STX input)
(fee-amount (/ (* stx-amount FEE-PERCENTAGE) u100))
(platform-fee (/ fee-amount u2))
(creator-fee (- fee-amount platform-fee))
(net-stx-amount (- stx-amount fee-amount))
)
;; Validation checks
(asserts! (var-get tradable) ERR-TRADING-DISABLED)
(asserts! (> stx-amount u0) ERR-INVALID-AMOUNT)
(asserts! (> tokens-out u0) ERR-INVALID-AMOUNT)
;; Transfer STX from buyer to DEX
(try! (stx-transfer? net-stx-amount tx-sender (as-contract tx-sender)))
;; Transfer fees
(if (> platform-fee u0)
(try! (stx-transfer? platform-fee tx-sender PLATFORM-FEE-ADDRESS))
true
)
(if (> creator-fee u0)
(try! (stx-transfer? creator-fee tx-sender (contract-of token-trait)))
true
)
;; Transfer tokens from DEX to buyer
(try! (as-contract (contract-call? token-trait transfer tokens-out tx-sender (tx-sender) none)))
(ok tokens-out)
)
)
What this does:
Uses constant product formula to calculate token output
Implements 2% fee split between platform and token creator
Transfers STX from buyer to DEX contract
Transfers calculated tokens from DEX to buyer
Includes comprehensive validation and error handling
3. Sell Function - Converting Tokens to STX
;; Sell tokens for STX using bonding curve
(define-public (sell (token-trait <sip-010-trait>) (tokens-in uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (unwrap-panic (contract-call? token-trait get-balance (as-contract tx-sender))))
;; Calculate STX to receive using constant product formula
;; stx_out = (stx_reserve * tokens_in) / (token_reserve + tokens_in)
(stx-out (/ (* current-stx-balance tokens-in) (+ current-token-balance tokens-in)))
;; Calculate fees (2% of STX output)
(fee-amount (/ (* stx-out FEE-PERCENTAGE) u100))
(platform-fee (/ fee-amount u2))
(creator-fee (- fee-amount platform-fee))
(net-stx-amount (- stx-out fee-amount))
)
;; Validation checks
(asserts! (var-get tradable) ERR-TRADING-DISABLED)
(asserts! (> tokens-in u0) ERR-INVALID-AMOUNT)
(asserts! (> net-stx-amount u0) ERR-INVALID-AMOUNT)
;; Transfer tokens from seller to DEX
(try! (contract-call? token-trait transfer tokens-in tx-sender (as-contract tx-sender) none))
;; Transfer STX from DEX to seller
(try! (as-contract (stx-transfer? net-stx-amount tx-sender (tx-sender))))
;; Transfer fees
(if (> platform-fee u0)
(try! (as-contract (stx-transfer? platform-fee tx-sender PLATFORM-FEE-ADDRESS)))
true
)
(if (> creator-fee u0)
(try! (as-contract (stx-transfer? creator-fee tx-sender (contract-of token-trait))))
true
)
(ok net-stx-amount)
)
)
What this does:
Calculates STX output using the same constant product formula
Deducts 2% fees from the STX output
Transfers tokens from seller to DEX contract
Transfers net STX amount to seller
Distributes fees to platform and creator
Read-Only Functions for Data Access
1. State Query Functions
;; Get current STX balance in the DEX
(define-read-only (get-stx-balance)
(stx-get-balance (as-contract tx-sender))
)
;; Get current token balance in the DEX
(define-read-only (get-token-balance)
(match (var-get associated-token)
token-contract (unwrap-panic (contract-call? token-contract get-balance (as-contract tx-sender)))
u0
)
)
;; Get initialization status
(define-read-only (get-initialized)
(var-get initialized)
)
;; Get trading status
(define-read-only (get-tradable)
(var-get tradable)
)
;; Get associated token contract
(define-read-only (get-associated-token)
(var-get associated-token)
)
What this does:
Provides external access to DEX state information
Allows frontends to display current liquidity
Enables monitoring of DEX status and configuration
2. Trading Preview Functions
;; Calculate how many tokens you get for a given STX amount
(define-read-only (get-buyable-tokens (stx-amount uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (get-token-balance))
)
(if (and (> current-stx-balance u0) (> current-token-balance u0))
(/ (* current-token-balance stx-amount) (+ current-stx-balance stx-amount))
u0
)
)
)
;; Calculate how much STX you get for a given token amount
(define-read-only (get-sellable-stx (token-amount uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (get-token-balance))
)
(if (and (> current-stx-balance u0) (> current-token-balance u0))
(/ (* current-stx-balance token-amount) (+ current-token-balance token-amount))
u0
)
)
)
;; Get graduation progress (0-100)
(define-read-only (get-progress)
(let ((current-stx (stx-get-balance (as-contract tx-sender))))
(if (>= current-stx TARGET-STX)
u100
(/ (* current-stx u100) TARGET-STX)
)
)
)
What this does:
get-buyable-tokens
: Preview tokens received for STX input (before fees)get-sellable-stx
: Preview STX received for token input (before fees)get-progress
: Shows how close the token is to "graduation" (0-100%)
Testing Our DEX Contract
Now let's thoroughly test our DEX to ensure it works correctly.
1. Check Contract Compilation
# Verify both contracts compile
clarinet check
You should see successful compilation for both token and DEX contracts.
2. Test with Clarinet Console
# Start interactive console
clarinet console
3. Initialize the DEX
;; First, let's check our starting balances
>> (stx-get-balance 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)
u100000000000000
>> (contract-call? .token get-balance 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)
(ok u100000000000000)
;; Transfer 50M tokens to the DEX for trading
>> (contract-call? .token transfer u50000000000000 tx-sender .dex none)
(ok true)
;; Initialize the DEX with token contract, token amount, and virtual STX
>> (contract-call? .dex initialize .token u50000000000000 u600000000)
(ok true)
What this shows:
Deployer starts with 100M tokens and 100,000 STX
We transfer 50M tokens to the DEX for trading liquidity
DEX initialization succeeds and enables trading
4. Verify DEX State
;; Check if DEX is initialized and tradable
>> (contract-call? .dex get-initialized)
(ok true)
>> (contract-call? .dex get-tradable)
(ok true)
;; Check DEX balances
>> (contract-call? .dex get-stx-balance)
(ok u0)
>> (contract-call? .dex get-token-balance)
(ok u50000000000000)
;; Check associated token
>> (contract-call? .dex get-associated-token)
(ok (some ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token))
What this shows: DEX is properly initialized with 50M tokens ready for trading.
5. Test Token Purchase
;; Switch to a different user for testing
>> ::set_tx_sender ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
;; Check how many tokens we can buy with 100 STX
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.dex get-buyable-tokens u100000000)
(ok u2885747938752)
;; Actually buy tokens with 100 STX
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.dex buy 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token u100000000)
(ok u2885747938752)
;; Check our new token balance
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token get-balance tx-sender)
(ok u2885747938752)
What this shows:
Preview function correctly calculates ~2.89M tokens for 100 STX
Buy transaction succeeds and transfers tokens to buyer
Bonding curve pricing is working correctly
6. Test Token Sale
;; Test selling 100M tokens back to the DEX
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.dex get-sellable-stx u100000000)
(ok u3597)
;; Actually sell 100M tokens
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.dex sell 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token u100000000)
(ok u3531)
;; Check updated balances
>> (stx-get-balance tx-sender)
u100000003531
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token get-balance tx-sender)
(ok u2785747938752)
What this shows:
Sell preview shows ~3,597 micro-STX for 100M tokens
Actual sale gives 3,531 micro-STX (after 2% fees)
Token balance decreases appropriately
7. Test Progress Tracking
;; Check graduation progress
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.dex get-progress)
(ok u3)
;; Check current STX balance in DEX
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.dex get-stx-balance)
(ok u98000000)
What this shows:
DEX has accumulated ~98 STX from trading
Progress is 3% toward the 3000 STX graduation target
Understanding the Results
Bonding Curve Mathematics
Our testing demonstrates the constant product formula in action:
Initial State: 50M tokens × 600 STX = 30B constant
After Buy: More STX in pool → fewer tokens → higher price for next buyer
After Sell: More tokens in pool → less STX → lower price for next seller
Fee Distribution
The 2% trading fee is automatically split:
1% goes to platform address for development/maintenance
1% goes to token creator as revenue sharing
Price Discovery
As more trades occur:
Buying pressure increases token price
Selling pressure decreases token price
Price reflects real supply and demand dynamics
Key Learning Points
Automated Market Maker Concepts
Constant Product Formula: Ensures liquidity is always available
Slippage: Large trades have bigger price impact
Virtual Liquidity: Enables immediate trading without upfront capital
Price Impact: Trade size affects final execution price
Smart Contract Security
Authorization Checks: Prevent unauthorized access
Input Validation: Ensure valid amounts and states
Atomic Operations: All-or-nothing transaction execution
Fee Handling: Secure and transparent fee distribution
Clarity Programming Patterns
Trait Usage: Generic interfaces for token interaction
State Management: Proper initialization and state tracking
Error Handling: Comprehensive error codes and validation
Read-Only Functions: Safe data access without state changes
Advanced Features Implemented
1. Dynamic Fee Calculation
;; Fee calculation with safety checks
(define-private (calculate-fees (amount uint))
(let (
(fee-amount (/ (* amount FEE-PERCENTAGE) u100))
(platform-fee (/ fee-amount u2))
(creator-fee (- fee-amount platform-fee))
)
{
total-fee: fee-amount,
platform-fee: platform-fee,
creator-fee: creator-fee,
net-amount: (- amount fee-amount)
}
)
)
2. Graduation System
When STX balance reaches TARGET-STX (3000 STX), the token has "graduated" and could be migrated to a full liquidity pool on other DEXs.
3. Virtual Liquidity
The virtual STX amount creates initial price stability and prevents extreme price swings on the first trades.
Next Steps
Congratulations! You've built a sophisticated bonding curve DEX with:
✅ Constant product AMM with automatic pricing
✅ Fee distribution system for sustainability
✅ Graduation mechanics for successful tokens
✅ Comprehensive state management and error handling
✅ Real-time data access for frontend integration
In Part 3 of this series, we'll build a modern Next.js frontend that provides a beautiful user interface for deploying tokens and trading through your DEX contracts.
What's Coming Next
Next.js 15 with App Router setup
Stacks wallet integration with @stacks/connect
Real-time trading interface with live price updates
Mobile-responsive design with shadcn/ui components
Complete DEX Contract Code
Here's the complete DEX contract for reference:
;; @title Bonding Curve DEX for STX.CITY Mini Version
;; @version 1.0
;; @summary A decentralized exchange facilitating token trading using bonding curve mechanism
;; @description This DEX allows users to buy and sell tokens through a bonding curve, with automatic liquidity provision
;; Import SIP-010 trait
(use-trait sip-010-trait 'STF0V8KWBS70F0WDKTMY65B3G591NN52PR4Z71Y3.sip-010-trait-ft-standard.sip-010-trait)
;; Error constants
(define-constant ERR-UNAUTHORIZED (err u401))
(define-constant ERR-INVALID-AMOUNT (err u402))
(define-constant ERR-INSUFFICIENT-BALANCE (err u403))
(define-constant ERR-SLIPPAGE-TOO-HIGH (err u404))
(define-constant ERR-TRADING-DISABLED (err u1001))
(define-constant ERR-ALREADY-INITIALIZED (err u1002))
(define-constant ERR-NOT-INITIALIZED (err u1003))
;; DEX Configuration
(define-constant TARGET-STX u3000000000) ;; 3000 STX target for graduation
(define-constant VIRTUAL-STX u600000000) ;; 600 STX virtual liquidity
(define-constant FEE-PERCENTAGE u2) ;; 2% trading fee
(define-constant PLATFORM-FEE-ADDRESS 'ST1WTA0YBPC5R6GDMPPJCEDEA6Z2ZEPNMQ4C39W6M)
;; State variables
(define-data-var associated-token (optional principal) none)
(define-data-var initialized bool false)
(define-data-var tradable bool false)
(define-data-var virtual-stx-amount uint VIRTUAL-STX)
;; Initialize the DEX with a token contract
(define-public (initialize (token-contract principal) (initial-token-amount uint) (initial-stx-amount uint))
(begin
(asserts! (not (var-get initialized)) ERR-ALREADY-INITIALIZED)
(var-set associated-token (some token-contract))
(var-set initialized true)
(var-set tradable true)
(var-set virtual-stx-amount initial-stx-amount)
(ok true)
)
)
;; Buy tokens with STX using bonding curve
(define-public (buy (token-trait <sip-010-trait>) (stx-amount uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (unwrap-panic (contract-call? token-trait get-balance (as-contract tx-sender))))
(tokens-out (/ (* current-token-balance stx-amount) (+ current-stx-balance stx-amount)))
(fee-amount (/ (* stx-amount FEE-PERCENTAGE) u100))
(platform-fee (/ fee-amount u2))
(creator-fee (- fee-amount platform-fee))
(net-stx-amount (- stx-amount fee-amount))
)
(asserts! (var-get tradable) ERR-TRADING-DISABLED)
(asserts! (> stx-amount u0) ERR-INVALID-AMOUNT)
(asserts! (> tokens-out u0) ERR-INVALID-AMOUNT)
(try! (stx-transfer? net-stx-amount tx-sender (as-contract tx-sender)))
(if (> platform-fee u0)
(try! (stx-transfer? platform-fee tx-sender PLATFORM-FEE-ADDRESS))
true
)
(if (> creator-fee u0)
(try! (stx-transfer? creator-fee tx-sender (contract-of token-trait)))
true
)
(try! (as-contract (contract-call? token-trait transfer tokens-out tx-sender (tx-sender) none)))
(ok tokens-out)
)
)
;; Sell tokens for STX using bonding curve
(define-public (sell (token-trait <sip-010-trait>) (tokens-in uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (unwrap-panic (contract-call? token-trait get-balance (as-contract tx-sender))))
(stx-out (/ (* current-stx-balance tokens-in) (+ current-token-balance tokens-in)))
(fee-amount (/ (* stx-out FEE-PERCENTAGE) u100))
(platform-fee (/ fee-amount u2))
(creator-fee (- fee-amount platform-fee))
(net-stx-amount (- stx-out fee-amount))
)
(asserts! (var-get tradable) ERR-TRADING-DISABLED)
(asserts! (> tokens-in u0) ERR-INVALID-AMOUNT)
(asserts! (> net-stx-amount u0) ERR-INVALID-AMOUNT)
(try! (contract-call? token-trait transfer tokens-in tx-sender (as-contract tx-sender) none))
(try! (as-contract (stx-transfer? net-stx-amount tx-sender (tx-sender))))
(if (> platform-fee u0)
(try! (as-contract (stx-transfer? platform-fee tx-sender PLATFORM-FEE-ADDRESS)))
true
)
(if (> creator-fee u0)
(try! (as-contract (stx-transfer? creator-fee tx-sender (contract-of token-trait))))
true
)
(ok net-stx-amount)
)
)
;; Read-only functions
(define-read-only (get-stx-balance)
(stx-get-balance (as-contract tx-sender))
)
(define-read-only (get-token-balance)
(match (var-get associated-token)
token-contract (unwrap-panic (contract-call? token-contract get-balance (as-contract tx-sender)))
u0
)
)
(define-read-only (get-initialized)
(var-get initialized)
)
(define-read-only (get-tradable)
(var-get tradable)
)
(define-read-only (get-associated-token)
(var-get associated-token)
)
(define-read-only (get-buyable-tokens (stx-amount uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (get-token-balance))
)
(if (and (> current-stx-balance u0) (> current-token-balance u0))
(/ (* current-token-balance stx-amount) (+ current-stx-balance stx-amount))
u0
)
)
)
(define-read-only (get-sellable-stx (token-amount uint))
(let (
(current-stx-balance (stx-get-balance (as-contract tx-sender)))
(current-token-balance (get-token-balance))
)
(if (and (> current-stx-balance u0) (> current-token-balance u0))
(/ (* current-stx-balance token-amount) (+ current-token-balance token-amount))
u0
)
)
)
(define-read-only (get-progress)
(let ((current-stx (stx-get-balance (as-contract tx-sender))))
(if (>= current-stx TARGET-STX)
u100
(/ (* current-stx u100) TARGET-STX)
)
)
)
(define-read-only (get-virtual-stx-amount)
(var-get virtual-stx-amount)
)
This DEX contract provides the foundation for a complete token trading ecosystem. In Part 3, we'll create a beautiful frontend that makes these powerful features accessible to everyday users through a modern web interface!
Subscribe to my newsletter
Read articles from Flames directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
