Building a Real-Time Crypto Tip Display with ESP32: The Ultimate Nano Cryptocurrency QR Code Scanner

Have you ever wanted to accept cryptocurrency tips in the physical world with instant visual feedback? Meet the ESP32 Nano Tipper – a sleek, portable device that generates QR codes for Nano cryptocurrency payments and provides real-time confirmation when tips are received. This project combines the power of ESP32 microcontrollers, TFT displays, and WebSocket technology to create a seamless crypto tipping experience.

πŸš€ Quick Flash – No Development Required!

Get started instantly with our web-based flasher: πŸ‘‰ Flash T-Display Nano Tipper – Web Flasher

Simply connect your TTGO T-Display board via USB and flash the firmware directly through your web browser – no Arduino IDE or development environment needed!


πŸš€ Project Overview

The ESP32 Nano Tipper is an IoT device that:

  • Displays QR codes for Nano cryptocurrency addresses
  • Monitors blockchain transactions in real-time via WebSockets
  • Shows instant gratitude screens when tips are received
  • Offers easy WiFi configuration through a captive portal
  • Stores settings persistently using SPIFFS file system

Perfect for content creators, street performers, small businesses, or anyone wanting to accept crypto tips with style!

πŸ› οΈ Hardware Requirements

  • TTGO T-Display ESP32 (with built-in TFT screen) – This specific board is required
  • Push button connected to GPIO 35 for configuration
  • WiFi connectivity for blockchain monitoring
  • Power source (USB or battery)

Important Note: This project is specifically designed for the TTGO T-Display board and has not been tested with other ESP32 variants. The display libraries and pin configurations are optimized for this particular hardware.

🎯 Key Features Breakdown

1. Smart WiFi Management with Configuration Portal

The device implements a sophisticated WiFi management system using the WiFiManager library. On startup, the system checks for a configuration button press on GPIO 35:

pinMode(BUTTON_CONFIG_PIN, INPUT_PULLUP);
bool forcePortal = false;
delay(200);
if (digitalRead(BUTTON_CONFIG_PIN) == LOW) {
    Serial.println("Button on GPIO35 pressed, forcing WiFiManager config portal.");
    forcePortal = true;
}

Configuration Process:

  • Automatic Connection: Attempts to connect to previously saved WiFi networks
  • Forced Configuration: Button press triggers immediate setup mode
  • Captive Portal: Creates “NanoTipper_Setup” access point at 192.168.4.1
  • Custom Parameters: Allows input of Nano cryptocurrency address during setup
  • Timeout Protection: 180-second portal timeout and 60-second connection timeout

The WiFiManager creates a custom parameter for the Nano address:

if (shouldSaveConfig) {
    Serial.println("Saving configuration (due to shouldSaveConfig flag)...");
    displayEnhancedMessage("SAVING", "Configuration...", "", "", ACCENT_COLOR, TEXT_COLOR, 1000);
    DynamicJsonDocument json(512);
    json["address"] = address;
    File configFile = SPIFFS.open("/config.json", "w");
    bool saveOk = false;
    if (!configFile) {
        Serial.println("Failed to open config file for writing");
        displayEnhancedMessage("ERROR", "Config Save", "Open FAILED!", "", ERROR_COLOR, TEXT_COLOR, 2000);
    } else {
        if (serializeJson(json, configFile) == 0) {
            Serial.println("Failed to write to config file");
            displayEnhancedMessage("ERROR", "Config Save", "Write Error!", "", ERROR_COLOR, TEXT_COLOR, 2000);
        } else {
            Serial.println("Custom configuration (address) saved to SPIFFS.");
            saveOk = true;
        }
        configFile.close();
    }
}

2. Enhanced Persistent Configuration Storage

The device uses the SPIFFS file system to store configuration data as JSON with comprehensive error handling:

Storage Features:

  • Automatic Backup: Configuration survives power cycles and resets
  • JSON Format: Human-readable configuration files
  • Error Handling: Comprehensive file operation error checking with visual feedback
  • Validation: Ensures Nano addresses start with “nano” or “xno” prefixes
  • Auto-restart: Device restarts after successful configuration save

3. Real-Time Blockchain Monitoring via Secure WebSockets

The heart of the system is a secure WebSocket connection to bitrequest.app on port 8010 with enhanced connection management:

void connectWebSocket() {
    Serial.println("Connecting to WebSocket (SSL)...");
    displayEnhancedMessage("CONNECTING", "WebSocket (SSL)", "", "", ACCENT_COLOR, TEXT_COLOR, 1000);
    
    if(webSocket.isConnected()) {
        webSocket.disconnect();
        wsConnected = false;
        wsSubscribed = false;
    }

    const char* ws_host = "bitrequest.app";
    const uint16_t ws_port = 8010;
    const char* ws_path = "/";
    
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi not connected. Cannot connect to WebSocket.");
        displayEnhancedMessage("ERROR", "WiFi not connected.", "Check settings.", "", ERROR_COLOR, TEXT_COLOR, 3000);
        return;
    }

    webSocket.beginSSL(ws_host, ws_port, ws_path);
    webSocket.onEvent(webSocketEvent);
    webSocket.setReconnectInterval(5000);
}

Subscription Mechanism: Once connected, the device sends a subscription message to monitor specific Nano addresses:

ws_tx_doc.clear();
ws_tx_doc["action"] = "subscribe";
ws_tx_doc["topic"] = "confirmation";
JsonObject options = ws_tx_doc.createNestedObject("options");
options["ack"] = true;
if (strlen(address) > 0) {
    options["accounts"][0] = address;
} else {
    Serial.println("[WSc] Nano address is empty, cannot subscribe.");
    return;
}

4. Advanced Amount Conversion Algorithm

Nano cryptocurrency uses a base unit system where 1 NANO = 10^30 raw units. The device implements a sophisticated conversion algorithm with enhanced precision:

const char* block_amount_raw_ptr = message["amount"].as<const char*>();
if (subtype && block_amount_raw_ptr && strcmp(subtype, "send") == 0) {
    Serial.printf("[WSc] Raw amount string received from server: '%s'\n", block_amount_raw_ptr);
    String amount_str = String(block_amount_raw_ptr);
    double nanoAmount = 0.0;
    int len = amount_str.length();
    String final_amount_str;
    
    if (len > 30) {
        final_amount_str = amount_str.substring(0, len - 30);
        final_amount_str += ".";
        final_amount_str += amount_str.substring(len - 30);
    } else {
        final_amount_str = "0.";
        for (int i = 0; i < (30 - len); i++) {
            final_amount_str += "0";
        }
        final_amount_str += amount_str;
    }
    
    Serial.printf("[WSc] String to convert to double: '%s'\n", final_amount_str.c_str());
    nanoAmount = atof(final_amount_str.c_str());
    Serial.printf("[WSc] Converted nanoAmount (double): %.12f\n", nanoAmount);
}

This algorithm handles amounts of any size, from tiny fractional tips to large whole number donations, maintaining precision up to 6 decimal places in the display.

5. Dynamic QR Code Generation and Display with Perfect Positioning

The QR code system uses a custom QR_ESP32 library with advanced layout calculations:

void drawMainScreen() {
    // Clear screen with blue background
    tft.fillScreen(0x051F); // Blue background (HIGHLIGHT_COLOR)
    
    // QR Code section - positioned at (1,1)
    int qrVersion = 0;
    int moduleSize = 4;
    String qrContent = "nano:" + String(address);
    
    int qrDrawX = 1; // Start at x=1
    int qrDrawY = 1; // Start at y=1
    
    // Calculate actual QR size first
    int actualQrSize = 0;
    try {
        qrcodegen::QrCode qr_temp = qrcodegen::QrCode::encodeText(qrContent.c_str(), qrcodegen::QrCode::Ecc::ECC_LOW);
        actualQrSize = qr_temp.getSize();
    } catch (...) {
        actualQrSize = 41; // Default estimate
    }
    
    int actualQrPixelWidth = actualQrSize * moduleSize;
    
    // QR background with white border - using ACTUAL QR size
    int qrBgWidth = actualQrPixelWidth + 2;
    int qrBgHeight = actualQrPixelWidth + 2;
    
    tft.fillRoundRect(qrDrawX - 1, qrDrawY - 1, qrBgWidth, qrBgHeight, 3, QR_BG_COLOR);
    tft.drawRoundRect(qrDrawX - 1, qrDrawY - 1, qrBgWidth, qrBgHeight, 3, TFT_WHITE);
    
    bool qrOk = QR_ESP32::drawQrCode(tft, qrContent.c_str(),
                                      qrDrawX, qrDrawY,
                                      moduleSize,
                                      qrVersion,
                                      qrcodegen::QrCode::Ecc::ECC_LOW,
                                      QR_MODULE_COLOR,
                                      QR_BG_COLOR);
}

QR Code Features:

  • Precise Positioning: Exact pixel calculation for optimal layout
  • Standard URI Format: Uses “nano:address” format for wallet compatibility
  • Natrium Wallet Integration: QR codes are optimized for Natrium wallet
  • Dynamic Sizing: Calculates actual QR dimensions before drawing
  • Error Correction: Low-level error correction for reliable scanning
  • Visual Enhancement: White rounded background with precise borders

6. Enhanced Multi-State Display System with Advanced Typography

The device implements a sophisticated state management system with multiple display states and enhanced visual feedback:

Enhanced Thank You Animation:

void displayEnhancedMessage(const char* title, const char* line1, const char* line2, const char* line3, uint16_t titleColor, uint16_t textColor, int delay_ms) {
    clearScreen();
    drawBorder(SECONDARY_COLOR);
    
    int titleHeight = 0;
    int contentStartY = 10;
    
    // Title section with background - DYNAMIC CALCULATION
    if (title && title[0]) {
        tft.setFreeFont(&FreeSans12pt7b);
        titleHeight = tft.fontHeight() + 10;  // Font height + padding
        tft.fillRect(2, 2, tft.width()-4, titleHeight, HIGHLIGHT_COLOR);
        tft.setTextColor(titleColor, HIGHLIGHT_COLOR);
        tft.setTextDatum(MC_DATUM);
        tft.drawString(title, tft.width()/2, 2 + titleHeight/2);
        contentStartY = titleHeight + 7;  // Spacing after title
    }

    // Content area - PERFECT CENTERING
    int contentEndY = tft.height() - 10;
    int availableHeight = contentEndY - contentStartY;
    
    tft.setTextColor(textColor, SCREEN_BG_COLOR);
    tft.setFreeFont(&FreeSans9pt7b);
    tft.setTextDatum(MC_DATUM);

    // Count and draw centered lines
    int line_count = 0;
    if (line1 && line1[0] && strlen(line1) > 0) line_count++;
    if (line2 && line2[0] && strlen(line2) > 0) line_count++;
    if (line3 && line3[0] && strlen(line3) > 0) line_count++;

    if (line_count > 0) {
        int line_height = tft.fontHeight() + 8;
        int total_text_height = line_count * line_height - 8;
        int text_start_y = contentStartY + (availableHeight - total_text_height) / 2;
        
        int current_y = text_start_y;
        if (line1 && line1[0] && strlen(line1) > 0) {
            tft.drawString(line1, tft.width()/2, current_y);
            current_y += line_height;
        }
        if (line2 && line2[0] && strlen(line2) > 0) {
            tft.drawString(line2, tft.width()/2, current_y);
            current_y += line_height;
        }
        if (line3 && line3[0] && strlen(line3) > 0) {
            tft.drawString(line3, tft.width()/2, current_y);
        }
    }
}

7. Intelligent State Management and Real-Time Status Updates

The main loop implements sophisticated state monitoring with visual feedback:

void thankYouScreen(double amount) {
    clearScreen();
    drawBorder(SUCCESS_COLOR);
    
    // Animated background effect
    for (int i = 0; i < 3; i++) {
        uint16_t animColor = (i % 2 == 0) ? HIGHLIGHT_COLOR : SUCCESS_COLOR;
        tft.drawRect(10 + i*3, 10 + i*3, tft.width() - 20 - i*6, tft.height() - 20 - i*6, animColor);
        delay(150);
    }
    
    // Clear animation traces
    tft.fillRect(15, 15, tft.width()-30, tft.height()-30, SCREEN_BG_COLOR);
    
    // Title background
    int titleHeight = 30;
    tft.fillRoundRect(10, 10, tft.width()-20, titleHeight, 5, SUCCESS_COLOR);
    
    // Thank you message
    tft.setTextColor(SCREEN_BG_COLOR, SUCCESS_COLOR);
    tft.setFreeFont(&FreeSans12pt7b);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("THANK YOU!", tft.width()/2, 10 + titleHeight/2);
    
    // Amount section with precise formatting
    char amountStr[30];
    snprintf(amountStr, sizeof(amountStr), "%.6f", amount);
    
    // Amount background
    int amountY = 50;
    int amountHeight = 50;
    tft.fillRoundRect(10, amountY, tft.width()-20, amountHeight, 5, HIGHLIGHT_COLOR);
    
    tft.setTextColor(WARNING_COLOR, HIGHLIGHT_COLOR);
    tft.setFreeFont(&FreeSans12pt7b);
    tft.drawString(amountStr, tft.width()/2, amountY + 18);
    
    tft.setTextColor(TEXT_COLOR, HIGHLIGHT_COLOR);
    tft.setFreeFont(&FreeSans9pt7b);
    tft.drawString("NANO", tft.width()/2, amountY + 38);
    
    delay(messageInterval > 0 ? messageInterval : 4000);
    redrawScreen = true;
}

🎨 Enhanced Visual Design Implementation

Advanced Color Palette

The device uses a carefully curated color scheme for optimal visibility and user experience:

void loop() {
    webSocket.loop();
    static unsigned long lastStatusUpdate = 0;
    unsigned long now = millis();

    if (redrawScreen) {
        if (wsConnected && wsSubscribed) {
            drawMainScreen();
        } else if (wsConnected && !wsSubscribed) {
            if (now - lastStatusUpdate > 2000) {
                displayEnhancedMessage("WEBSOCKET", "Connected", "Subscribing...", "", SUCCESS_COLOR, TEXT_COLOR, 0);
                lastStatusUpdate = now;
            }
        } else {
             if (now - lastStatusUpdate > 2000) {
                 if (WiFi.status() != WL_CONNECTED && WiFi.getMode() != WIFI_AP) {
                    displayEnhancedMessage("NETWORK", "WiFi", "Connecting...", "", WARNING_COLOR, TEXT_COLOR, 0);
                 } else if (WiFi.isConnected() && !wsConnected) {
                    displayEnhancedMessage("WEBSOCKET", "Connecting...", "", "", WARNING_COLOR, TEXT_COLOR, 0);
                 } else if (WiFi.getMode() == WIFI_AP) {
                    // Already handled by setAPCallback
                 } else {
                    displayEnhancedMessage("ERROR", "Network Error", "Check Connection", "", ERROR_COLOR, TEXT_COLOR, 0);
                 }
                lastStatusUpdate = now;
            }
        }
        redrawScreen = false;
    }
    delay(50);
}

Enhanced Typography System

The device employs a sophisticated font system with dynamic sizing:

  • FreeSans12pt7b: For titles and important information
  • FreeSans9pt7b: For body text and status messages
  • Dynamic font height calculation: tft.fontHeight() for precise positioning
  • Perfect text alignment: MC_DATUM (middle-center) for professional appearance
  • Responsive positioning: Mathematical center calculation for all elements

πŸ” Enhanced Security and Reliability Features

Network Security

  • SSL/TLS encryption: All WebSocket communications use secure connections
  • Connection state validation: Comprehensive WiFi status checking before WebSocket attempts
  • Input validation: Strict checking of address formats and lengths
  • Timeout protection: Prevents hanging connections and portal abuse

Data Integrity

  • Enhanced JSON validation: Comprehensive error checking for all parsed data with visual feedback
  • File system robustness: SPIFFS mount verification and detailed error recovery
  • Transaction verification: Confirms transaction types and validates amounts before processing
  • Address validation: Multiple format checks for proper Nano address format

Operational Reliability

  • Automatic recovery: System restarts on critical configuration errors with user notification
  • Connection monitoring: Continuous status checking with automatic reconnection and visual feedback
  • State persistence: Important settings survive power cycles with error handling
  • Graceful degradation: System continues operating even with partial functionality

πŸš€ Easy Installation Methods

πŸ”— Flash ESP32 Nano Tipper – Web Flasher

  1. Visit the web flasher link Connect your TTGO T-Display board via USB
  2. Click “INSTALL NANOTIPPER”
  3. Connect your TTGO T-Display board via USB and select your device
  4. Press “Install NanoTipper” and wait 2 minutes
  5. Your device is ready to configure!

Flashing Requirements:

  • TTGO T-Display board only – The flashing portal is designed exclusively for this hardware
  • Web browser with WebSerial support (Chrome, Edge, Opera)
  • USB cable for device connection
  • No development environment needed – Direct flashing from browser

Method 2: Manual Development Setup

Library Dependencies

#include <Arduino.h>            // Core Arduino functionality
#include <WiFi.h>               // ESP32 WiFi functionality
#include <WiFiManager.h>        // Captive portal configuration
#include <WebSocketsClient.h>   // WebSocket communication
#include <ArduinoJson.h>        // JSON parsing (v7.x required)
#include <TFT_eSPI.h>           // Display control
#include <SPIFFS.h>             // File system
#include "QR_ESP32.h"           // Custom QR code generation library

Hardware Setup

  • TTGO T-Display board with integrated TFT (specific model required)
  • Configuration button wired to GPIO 35 with pull-up resistor
  • Stable power supply (USB or battery pack)
  • WiFi network access for blockchain monitoring

🎯 Use Cases and Applications

Content Creation

  • Live streaming: Accept tips during broadcasts with instant visual acknowledgment
  • Video content: Display QR codes in videos for viewer donations with real-time feedback
  • Social media: Physical device for in-person content creation and fan interaction

Street Performance

  • Busking: Modern, cashless tip collection with instant gratitude display
  • Art displays: Interactive donation system for public art installations
  • Music performance: Silent tip notification system that doesn’t interrupt performance

Small Business

  • Retail: Alternative payment acceptance with visual confirmation
  • Services: Professional service tip collection with client-friendly interface
  • Events: Donation drives and fundraising with transparent amount display

Educational

  • Blockchain demos: Live demonstration of cryptocurrency transactions in real-time
  • Tech education: Hands-on IoT and blockchain integration learning tool
  • Workshops: Interactive learning device for crypto and embedded systems concepts

πŸ“Š Technical Architecture Summary

Enhanced System Flow

  1. Startup: Device initializes display with visual feedback, checks configuration button, mounts SPIFFS
  2. Configuration: Loads saved settings with error handling or starts WiFiManager portal with visual guidance
  3. Network Connection: Connects to WiFi with status display and establishes secure WebSocket connection
  4. Subscription: Subscribes to blockchain confirmations for specified address with acknowledgment
  5. Operation: Displays QR code with perfect positioning and monitors for incoming transactions
  6. Transaction Processing: Parses amounts with precision, displays animated thank you screens
  7. Continuous Monitoring: Maintains connections with visual status updates and handles state changes gracefully

Memory Management

  • Static JSON documents: Pre-allocated for WebSocket messages (512B TX, 1024B RX)
  • Efficient string handling: Optimized string operations for amount conversion and display
  • Display buffers: TFT_eSPI library manages display memory automatically
  • SPIFFS allocation: Minimal file system usage with comprehensive error handling

Performance Characteristics

  • Boot time: ~3-5 seconds from power-on to operational with visual feedback
  • Response time: Sub-second transaction notification display with animations
  • Memory usage: Optimized for ESP32 constraints with efficient resource management
  • Power consumption: Minimal when idle, brief spikes during network activity and display updates

πŸ”§ Configuration and Setup

Initial Configuration

  1. Flash the firmware using the web flasher or manual setup
  2. Power on the device – it will display startup messages
  3. Press the GPIO 35 button during startup to force configuration mode (optional)
  4. Connect to WiFi network “NanoTipper_Setup” if in configuration mode
  5. Navigate to 192.168.4.1 in your browser
  6. Enter your WiFi credentials and Nano address (nano or xno format)
  7. Save configuration – device will restart automatically

Address Format Requirements

  • Must start with nano_ or xno_
  • Minimum 60 characters total length
  • Standard Nano cryptocurrency address format
  • Validated before saving to prevent errors

Button Operations

  • GPIO 35 button during startup: Forces WiFiManager configuration portal
  • Automatic operation: Device remembers settings and connects automatically on subsequent boots

Wallet Compatibility:

  • Primary Support: Natrium wallet (tested and verified)
  • Automatic Opening: QR codes will automatically launch Natrium if installed on the scanning device
  • Other Wallets: Compatibility with other Nano wallets has not been extensively tested

πŸ“š Conclusion

The ESP32 Nano Tipper V5 represents a complete embedded systems solution that elegantly bridges the gap between digital blockchain technology and physical world interactions. With enhanced error handling, sophisticated state management, advanced display features, and easy web-based flashing, this project demonstrates the practical application of modern IoT development principles.

The combination of secure WebSocket communication, real-time blockchain monitoring, dynamic QR code generation, and intuitive user interface creates a professional-grade device suitable for various applications from content creation to small business operations.

Get started today with the web flasher: https://nanolover.online/nanotipper-flash/


This project showcases how embedded systems can create meaningful interactions between blockchain technology and the physical world, providing users with immediate, tangible feedback from digital cryptocurrency transactions.