Understanding Block Height in Stacks Blockchain

Mastering Time-Based Behavior in Clarity with
stacks-block-height
Introduction: A World Without Clocks
Time is fundamental to most applications: subscription renewals, voting deadlines, ticket expirations, auction closings. But on the Stacks blockchain, Clarity doesn't expose real-time timestamps. There’s no block.timestamp
or now
like in Solidity.
Instead, Clarity gives you something better: determinism.
Block height is a number that represents a block’s position in a blockchain’s sequence of blocks. It’s like a page number in a book, starting from the genesis block (height 0 or 1, depending on the blockchain) and increasing by 1 for each new block added.
With stacks-block-height
, you get a reliable, predictable number that increases with every new block (~10 minutes). If you think of time in block intervals instead of seconds, you can build nearly any time-based system with absolute trust and zero reliance on external data.
This article explores how to harness stacks-block-height
to build:
Expiring NFT tickets
Subscriptions and recurring payments
Escrow systems with dispute deadlines
Let’s reimagine how we tell time on-chain.
The Building Block: stacks-block-height
(print stacks-block-height)
;; => u132120 (for example)
This value increments by 1 with every block mined. Since blocks are mined every 10 seconds on average, you can calculate future events in 10-seconds increments.
Why This Matters
Deterministic: Everyone sees the same value at the same block.
Secure: No reliance on oracles or external timestamps.
Block-Based Time: Plan events in blocks (e.g., "100 blocks from now" ≈ 1,000 seconds or ~16.6 minutes).
Use Case 1: Auto-Expiring NFT Tickets
Say you’re building a decentralized ticketing platform for concerts.
Tickets should be claimable for 100 blocks (~16.6 minutes). After that, they expire.
(define-constant EXPIRY_BLOCKS u100)
(define-data-var ticket-counter uint u0)
(define-map tickets
{ ticket-id: uint }
{ owner: principal, claimed: bool, expires-at: uint })
(define-public (create-ticket (owner principal))
(let ((ticket-id (+ (var-get ticket-counter) u1)))
(var-set ticket-counter ticket-id)
(map-set tickets { ticket-id: ticket-id }
{ owner: owner, claimed: false, expires-at: (+ stacks-block-height EXPIRY_BLOCKS) })
(ok ticket-id)))
(define-public (claim-ticket (ticket-id uint))
(let ((ticket (unwrap! (map-get? tickets { ticket-id: ticket-id }) (err u404))))
(asserts! (not (get claimed ticket)) (err u403))
(asserts! (<= stacks-block-height (get expires-at ticket)) (err u410))
(map-set tickets { ticket-id: ticket-id } (merge ticket { claimed: true }))
(ok true)))
Tip: Use block-based expiry for auction deadlines or access tokens too.
Use Case 2: Recurring Subscription Payments
Let’s say you’re building a Patreon-like app where users can subscribe to support creators.
(define-map subscriptions
{ id: uint }
{
subscriber: principal,
creator: principal,
amount: uint,
interval: uint,
next-due: uint })
(define-public (create-subscription
(creator principal)
(amount uint)
(interval uint))
(let ((id (some-id)))
(map-set subscriptions { id: id } {
subscriber: tx-sender,
creator: creator,
amount: amount,
interval: interval,
next-due: (+ stacks-block-height interval)
})
(ok id)))
(define-public (pay-subscription (id uint))
(let ((sub (unwrap! (map-get? subscriptions { id: id }) (err u404))))
(asserts! (>= stacks-block-height (get next-due sub)) (err u401))
(try! (stx-transfer? (get amount sub) (get subscriber sub) (get creator sub)))
(map-set subscriptions { id: id } (merge sub {
next-due: (+ stacks-block-height (get interval sub))
}))
(ok true)))
You can use the same logic for rent, salaries, DAO salaries, or staking rewards.
Use Case 3: Escrow With Dispute Deadlines
You’re building an escrow dApp. Funds can be disputed within 50 blocks (8.3 minutes) of creation.
(define-map escrows
{ id: uint }
{
buyer: principal,
seller: principal,
amount: uint,
created-at: uint,
dispute-deadline: uint,
status: uint })
(define-constant DISPUTE_WINDOW u50)
(define-public (create-escrow (seller principal) (amount uint))
(let ((id (some-id)))
(try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
(map-set escrows { id: id } {
buyer: tx-sender,
seller: seller,
amount: amount,
created-at: stacks-block-height,
dispute-deadline: (+ stacks-block-height DISPUTE_WINDOW),
status: u0
})
(ok id)))
(define-public (raise-dispute (id uint))
(let ((escrow (unwrap! (map-get? escrows { id: id }) (err u404))))
(asserts! (<= stacks-block-height (get dispute-deadline escrow)) (err u410))
(map-set escrows { id: id } (merge escrow { status: u2 }))
(ok true)))
🔗 This block-height pattern is powerful for governance, contests, or fund disbursement schedules.
Pro Tips & Patterns
Use named constants (
EXPIRE_BLOCKS
,INTERVAL
,DISPUTE_WINDOW
) for readabilityConsider creating a utility function:
(define-private (has-expired? (deadline uint)) (> stacks-block-height deadline))
For off-chain display, calculate real-world time from block height (~10 seconds per block)
Use consistent error codes (e.g., u404 for not found, u410 for expired)
Conclusion: Time Is What You Make of It
Clarity teaches us that "time" doesn't have to mean "now."
By working with stacks-block-height
, you're building logic that is:
Predictable
Verifiable
Resistant to manipulation
And that’s what makes decentralized systems truly powerful.
The next time you need a timestamp, ask: “How many blocks from now?”
Want to see real examples? Check out StackPay.
Subscribe to my newsletter
Read articles from Samuel Dahunsi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
