At Verizon, I developed a full-stack NFT collectibles solution for AMC's Walking Dead franchise. This wasn't just another blockchain project - it was a production system that had to handle real users, real transactions, and real money. Here's what I learned.
The Architecture Challenge
Blockchain interactions are fundamentally different from traditional API calls. They're slow, expensive, and can fail in unpredictable ways. We needed an architecture that could:
- Handle blockchain latency without blocking users
- Manage transaction costs (gas fees)
- Provide a responsive user experience
- Scale to thousands of concurrent users
The Request Queue System
Our solution was a request scheduling system using Redis and Socket.io:
// blockchain-queue.service.ts
import Redis from 'ioredis';
import { Server as SocketServer } from 'socket.io';
import { ethers } from 'ethers';
interface BlockchainRequest {
id: string;
type: 'mint' | 'transfer' | 'query';
userId: string;
params: any;
priority: number;
timestamp: number;
}
export class BlockchainQueueService {
private redis: Redis;
private io: SocketServer;
private provider: ethers.providers.Provider;
private contract: ethers.Contract;
private processing = false;
private maxConcurrent = 3;
private activeRequests = 0;
constructor(
redisClient: Redis,
socketServer: SocketServer,
contractAddress: string,
contractABI: any
) {
this.redis = redisClient;
this.io = socketServer;
this.provider = new ethers.providers.JsonRpcProvider(
process.env.ETHEREUM_RPC_URL
);
this.contract = new ethers.Contract(
contractAddress,
contractABI,
this.provider
);
}
async enqueueRequest(request: Omit<BlockchainRequest, 'id' | 'timestamp'>): Promise<string> {
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const fullRequest: BlockchainRequest = {
id: requestId,
timestamp: Date.now(),
...request
};
// Add to sorted set by priority
await this.redis.zadd(
'blockchain_queue',
fullRequest.priority,
JSON.stringify(fullRequest)
);
// Start processing if not already running
if (!this.processing) {
this.processQueue();
}
return requestId;
}
private async processQueue(): Promise<void> {
this.processing = true;
while (await this.redis.zcard('blockchain_queue') > 0) {
// Wait if we're at max concurrent requests
while (this.activeRequests >= this.maxConcurrent) {
await new Promise(resolve => setTimeout(resolve, 100));
}
// Get highest priority request
const items = await this.redis.zpopmax('blockchain_queue');
if (!items || items.length === 0) continue;
const [requestJson] = items;
const request: BlockchainRequest = JSON.parse(requestJson);
// Process request without awaiting (handle concurrently)
this.executeRequest(request);
}
this.processing = false;
}
private async executeRequest(request: BlockchainRequest): Promise<void> {
this.activeRequests++;
try {
let result;
switch (request.type) {
case 'mint':
result = await this.mintNFT(request.params);
break;
case 'transfer':
result = await this.transferNFT(request.params);
break;
case 'query':
result = await this.queryNFT(request.params);
break;
default:
throw new Error(`Unknown request type: ${request.type}`);
}
// Emit success to specific user
this.io.to(request.userId).emit(`request_${request.id}`, {
status: 'success',
data: result
});
// Store result in Redis for 24 hours
await this.redis.setex(
`result_${request.id}`,
86400,
JSON.stringify({ status: 'success', data: result })
);
} catch (error) {
console.error(`Request ${request.id} failed:`, error);
this.io.to(request.userId).emit(`request_${request.id}`, {
status: 'error',
error: error.message
});
await this.redis.setex(
`result_${request.id}`,
86400,
JSON.stringify({ status: 'error', error: error.message })
);
} finally {
this.activeRequests--;
}
}
private async mintNFT(params: { to: string; tokenId: string; metadata: string }): Promise<any> {
const wallet = new ethers.Wallet(
process.env.MINTING_PRIVATE_KEY,
this.provider
);
const contractWithSigner = this.contract.connect(wallet);
// Estimate gas
const gasLimit = await contractWithSigner.estimateGas.mint(
params.to,
params.tokenId,
params.metadata
);
// Add 20% buffer
const gasLimitWithBuffer = gasLimit.mul(120).div(100);
const tx = await contractWithSigner.mint(
params.to,
params.tokenId,
params.metadata,
{
gasLimit: gasLimitWithBuffer
}
);
// Wait for confirmation
const receipt = await tx.wait(2); // Wait for 2 confirmations
return {
transactionHash: receipt.transactionHash,
blockNumber: receipt.blockNumber,
tokenId: params.tokenId
};
}
private async transferNFT(params: { from: string; to: string; tokenId: string }): Promise<any> {
const tx = await this.contract.transferFrom(
params.from,
params.to,
params.tokenId
);
const receipt = await tx.wait(2);
return {
transactionHash: receipt.transactionHash,
blockNumber: receipt.blockNumber
};
}
private async queryNFT(params: { tokenId: string }): Promise<any> {
const owner = await this.contract.ownerOf(params.tokenId);
const tokenURI = await this.contract.tokenURI(params.tokenId);
// Fetch metadata
const metadata = await fetch(tokenURI).then(res => res.json());
return {
tokenId: params.tokenId,
owner,
metadata
};
}
}API Endpoints
The REST API provided a simple interface for the frontend:
// blockchain.controller.ts
import { Request, Response } from 'express';
import { BlockchainQueueService } from './blockchain-queue.service';
export class BlockchainController {
constructor(private queueService: BlockchainQueueService) {}
async mintNFT(req: Request, res: Response) {
const { to, metadata } = req.body;
const userId = req.user.id;
try {
const tokenId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const requestId = await this.queueService.enqueueRequest({
type: 'mint',
userId,
params: { to, tokenId, metadata },
priority: 10 // High priority for minting
});
res.json({
requestId,
message: 'Minting request queued. You will be notified when complete.',
estimatedTime: '2-5 minutes'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getUserNFTs(req: Request, res: Response) {
const { address } = req.params;
try {
const requestId = await this.queueService.enqueueRequest({
type: 'query',
userId: req.user.id,
params: { address },
priority: 1 // Lower priority for queries
});
res.json({
requestId,
message: 'Query queued'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getRequestStatus(req: Request, res: Response) {
const { requestId } = req.params;
try {
const result = await this.queueService.getRequestResult(requestId);
if (!result) {
res.json({ status: 'pending' });
} else {
res.json(result);
}
} catch (error) {
res.status(500).json({ error: error.message });
}
}
}React Native Frontend
The mobile app used Socket.io for real-time updates:
// useBlockchainRequest.ts
import { useState, useEffect } from 'react';
import io from 'socket.io-client';
export function useBlockchainRequest(requestId: string | null) {
const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending');
const [data, setData] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!requestId) return;
const socket = io(process.env.API_URL);
socket.on(`request_${requestId}`, (response) => {
if (response.status === 'success') {
setStatus('success');
setData(response.data);
} else {
setStatus('error');
setError(response.error);
}
});
return () => {
socket.disconnect();
};
}, [requestId]);
return { status, data, error };
}
// MintNFTScreen.tsx
export function MintNFTScreen() {
const [requestId, setRequestId] = useState<string | null>(null);
const { status, data, error } = useBlockchainRequest(requestId);
const handleMint = async () => {
const response = await fetch(`${API_URL}/blockchain/mint`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
to: userAddress,
metadata: nftMetadata
})
});
const { requestId } = await response.json();
setRequestId(requestId);
};
return (
<View>
<Button title="Mint NFT" onPress={handleMint} />
{status === 'pending' && (
<ActivityIndicator size="large" />
)}
{status === 'success' && (
<Text>NFT Minted! Transaction: {data.transactionHash}</Text>
)}
{status === 'error' && (
<Text>Error: {error}</Text>
)}
</View>
);
}Key Lessons
- Never block the UI: Blockchain operations are slow - always queue them
- Gas estimation is critical: Running out of gas wastes money
- Confirmations matter: Wait for multiple confirmations for important transactions
- Error handling is complex: Network issues, gas issues, smart contract reverts - handle them all
- Test on testnet extensively: Real ETH is expensive - test thoroughly first
Conclusion
Blockchain development requires rethinking traditional application architecture. The key is building systems that gracefully handle the asynchronous, expensive nature of blockchain interactions while providing a great user experience.
The queue-based approach we built at Verizon scaled well and kept users happy even during high load periods.

Jason Cochran
Sofware Engineer | Cloud Consultant | Founder at Strataga
27 years of experience building enterprise software for oil & gas operators and startups. Specializing in SCADA systems, field data solutions, and AI-powered rapid development. Based in Midland, TX serving the Permian Basin.