Understanding Block Height in Stacks Blockchain

Samuel DahunsiSamuel Dahunsi
4 min read

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 readability

  • Consider 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.

0
Subscribe to my newsletter

Read articles from Samuel Dahunsi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Samuel Dahunsi
Samuel Dahunsi