Build a professional Web3 application with wallet authentication, smart contracts, and blockchain integration - No blockchain experience required!
A complete decentralized application (dApp) with:
- Frontend: React + TypeScript + Vite
- Wallet Auth: Privy for seamless wallet connections
- Blockchain: Ethereum/Polygon smart contract integration
- Web3 Library: ethers.js or viem
- Features: Wallet connect, NFT minting, token interactions
- Deployment: Vercel with blockchain connectivity
Time Required: 60-75 minutes
Traditional Web (Web2):
- Your data lives on company servers
- Companies control access
- You need username/password
Web3:
- Your data lives in your wallet (you control it)
- You connect with your wallet (like MetaMask)
- No passwords needed - cryptographic signatures prove it's you
Privy Makes This Easy:
- Users can connect with any wallet
- Or sign in with email/social (Privy creates wallet for them)
- Handles all complexity automatically
- Basic React knowledge helpful (but not required)
- MetaMask or any Web3 wallet installed
- Terminal/command line access
# Create project folder
mkdir my-web3-dapp
cd my-web3-dapp
# Download Sidekick
git clone https://github.com/dylanburkey/claude-code-sidekick.git temp-sidekick
cd temp-sidekick
# Copy Sidekick files
cp -r .claude/ ../
cp PROJECT_STARTER.md ../
cp .env.example ../
# Clean up
cd ..
rm -rf temp-sidekick## Project Information
### Project Name
NFT Minting dApp
### Project Description
A decentralized application for minting NFTs with wallet authentication via
Privy. Users connect their wallet, interact with smart contracts, and mint their
own NFTs on the Polygon network.
### Project Type
web-app### Project Preset
- [ ] **Static Website**
- [ ] **Astro Site**
- [x] **React App** - React, TypeScript, Vite, TanStack
- [ ] **Next.js App**
- [ ] **Vue/Nuxt**
- [ ] **SvelteKit**
- [ ] **Full Stack**
- [ ] **Custom**## Code Rules Configuration
### Language Standards
- **Modern JavaScript**: `TRUE`
- **TypeScript**: `TRUE` ← Important for Web3
- **Node.js**: `TRUE`
### Framework Standards
- **React**: `TRUE`
### Quality & Testing
- **Security**: `TRUE` ← Critical for Web3
- **Testing Standards**: `TRUE`
## MCP Configuration
### Development Tools
- **GitHub**: `TRUE`
- **Sentry**: `TRUE` ← Track Web3 errors
### Cloud & Infrastructure
- **Vercel**: `TRUE` ← For deployment/quick-start- Go to https://privy.io
- Click "Get Started" or "Sign Up"
- Create account with email
- Verify your email
- In Privy Dashboard, click "Create App"
- App Name: "NFT Minting dApp"
- Select network: "Polygon Mumbai" (testnet for development)
- Click "Create"
In the Privy Dashboard:
Login Methods:
- ✅ Wallet (MetaMask, Coinbase, WalletConnect)
- ✅ Social (Google, Twitter, Discord)
Chains:
- ✅ Polygon Mumbai (for development)
- Later add Polygon Mainnet (for production)
Appearance:
- Upload your logo
- Set brand colors
- Customize connect modal
- In Dashboard → Settings → API Keys
- Copy your "App ID" (starts with
clp...) - Save it - you'll need this!
- Go to https://www.alchemy.com
- Create free account
- Create new app:
- Chain: Polygon
- Network: Mumbai (testnet)
- Copy your API key
Create .env file:
cp .env.example .envAdd your keys:
# Privy
VITE_PRIVY_APP_ID=your_privy_app_id_here
# Alchemy (Polygon Mumbai)
VITE_ALCHEMY_API_KEY=your_alchemy_key_here
VITE_ALCHEMY_RPC_URL=https://polygon-mumbai.g.alchemy.com/v2/YOUR_KEY
# Contract Address (we'll deploy later)
VITE_NFT_CONTRACT_ADDRESS=
# Network
VITE_CHAIN_ID=80001 # Polygon Mumbai
VITE_CHAIN_NAME=Polygon Mumbai
# App
VITE_APP_URL=http://localhost:5173
# GitHub (for deployment)
GITHUB_TOKEN=your_github_token
# Vercel (for deployment)
VERCEL_TOKEN=your_vercel_tokenIn PROJECT_STARTER.md:
## Goals & Objectives
### Primary Goal
Create a Web3 NFT minting application where users can:
1. **Wallet Authentication**
- Connect with MetaMask, Coinbase, WalletConnect
- Sign in with email (Privy creates wallet)
- Social login (Google, Twitter, Discord)
- View wallet address and balance
2. **NFT Minting**
- Mint new NFTs with custom metadata
- Upload image to IPFS
- Pay gas fees in MATIC
- View minting transaction on blockchain explorer
- See minted NFT in collection
3. **NFT Gallery**
- View all minted NFTs
- Display NFT metadata (name, description, image)
- Show owner address
- Link to OpenSea/marketplace
4. **Wallet Features**
- View MATIC balance
- View NFT collection
- Switch between wallets
- Disconnect wallet
- Transaction history
### Success Criteria
- Users can connect wallet in under 10 seconds
- Minting transaction completes in under 30 seconds
- NFT appears in gallery immediately after minting
- Works on mobile devices
- Handles errors gracefully (insufficient funds, rejected transactions)
- Responsive design
### Functional Requirements
1. **WHEN** user clicks "Connect Wallet" **THE SYSTEM SHALL** show Privy modal
with login options
2. **WHEN** user connects wallet **THE SYSTEM SHALL** fetch and display their
address and balance
3. **WHEN** user mints NFT **THE SYSTEM SHALL** prompt for transaction signature
4. **WHEN** transaction succeeds **THE SYSTEM SHALL** show success message with
transaction hash
5. **WHEN** user insufficient funds **THE SYSTEM SHALL** show friendly error
message
6. **IF** user rejects transaction **THEN THE SYSTEM SHALL** allow them to try
again
7. **THE SYSTEM SHALL** store NFT metadata on IPFS
8. **THE SYSTEM SHALL** display loading states during blockchain operations
9. **THE SYSTEM SHALL** show gas estimates before transactions
10. **THE SYSTEM SHALL** work on Polygon Mumbai testnetnpm install --save \
@privy-io/react-auth \
@privy-io/wagmi-connector \
wagmi \
viem \
@tanstack/react-queryWhat each does:
@privy-io/react-auth- Privy wallet connectionwagmi- React hooks for Ethereumviem- Modern Ethereum library@tanstack/react-query- Data fetching
/project-plannerCreates a plan covering:
- React + TypeScript setup with Vite
- Privy integration
- Smart contract deployment
- NFT minting functionality
- IPFS integration for metadata
- Wallet state management
- UI components
/task-plannerTasks will include:
- Set up React + TypeScript + Vite
- Install and configure Privy
- Create wallet connection flow
- Build smart contract (Solidity)
- Deploy contract to Polygon Mumbai
- Create minting interface
- Integrate IPFS for metadata
- Build NFT gallery
- Add transaction feedback
- Deploy to Vercel
/task-runnerWatch progress:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Task Runner Started
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[React Agent] Task 1: Set up React + Vite
→ Creating Vite project with React + TypeScript...
→ Configuring TypeScript strict mode...
→ Setting up path aliases...
✓ Complete (8.2s)
[Blockchain Agent] Task 2: Configure Privy
→ Installing Privy dependencies...
→ Creating PrivyProvider wrapper...
→ Configuring chains and login methods...
✓ Complete (12.4s)
[Blockchain Agent] Task 3: Create smart contract
→ Writing ERC721 NFT contract...
→ Adding minting function...
→ Adding metadata support...
→ Compiling with Hardhat...
✓ Complete (18.7s)
[Blockchain Agent] Task 4: Deploy contract
→ Configuring Hardhat for Polygon Mumbai...
→ Deploying NFTContract...
→ Contract deployed to: 0x1234...
→ Verifying on Polygonscan...
✓ Complete (45.3s)
[React Agent] Task 5: Build minting UI
→ Creating MintNFT component...
→ Adding form with image upload...
→ Integrating with smart contract...
→ Adding transaction feedback...
✓ Complete (16.8s)
[React Agent] Task 6: Build NFT gallery
→ Creating NFTGallery component...
→ Fetching NFTs from contract...
→ Displaying metadata and images...
✓ Complete (14.2s)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
All Tasks Complete!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Contract Address: 0x1234567890abcdef...
Deployed to: Polygon Mumbai
View on Polygonscan: https://mumbai.polygonscan.com/address/0x...
my-web3-dapp/
├── src/
│ ├── main.tsx # App entry point
│ ├── App.tsx # Main app component
│ ├── providers/
│ │ └── PrivyProvider.tsx # Privy configuration
│ ├── components/
│ │ ├── ConnectWallet.tsx # Wallet connection button
│ │ ├── WalletInfo.tsx # Display wallet details
│ │ ├── MintNFT.tsx # Minting interface
│ │ ├── NFTGallery.tsx # Display NFTs
│ │ └── NFTCard.tsx # Individual NFT display
│ ├── hooks/
│ │ ├── useWallet.ts # Wallet state hook
│ │ ├── useMintNFT.ts # Minting logic
│ │ └── useNFTs.ts # Fetch NFTs
│ ├── lib/
│ │ ├── contract.ts # Contract interaction
│ │ ├── ipfs.ts # IPFS upload
│ │ └── config.ts # Chain config
│ └── contracts/
│ ├── NFT.sol # Smart contract
│ └── NFT.json # Contract ABI
├── hardhat/
│ ├── contracts/ # Solidity contracts
│ ├── scripts/ # Deployment scripts
│ └── hardhat.config.ts
├── .env
├── package.json
└── vite.config.ts
src/providers/PrivyProvider.tsx:
import { PrivyProvider } from '@privy-io/react-auth';
import { WagmiConfig, createConfig } from 'wagmi';
import { polygonMumbai } from 'wagmi/chains';
const wagmiConfig = createConfig({
chains: [polygonMumbai],
transports: {
[polygonMumbai.id]: http(
`https://polygon-mumbai.g.alchemy.com/v2/${import.meta.env.VITE_ALCHEMY_API_KEY}`
),
},
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={import.meta.env.VITE_PRIVY_APP_ID}
config={{
loginMethods: ['wallet', 'email', 'google', 'twitter'],
appearance: {
theme: 'light',
accentColor: '#6366f1',
logo: '/logo.png',
},
embeddedWallets: {
createOnLogin: 'users-without-wallets',
},
}}
>
<WagmiConfig config={wagmiConfig}>
{children}
</WagmiConfig>
</PrivyProvider>
);
}src/components/ConnectWallet.tsx:
import { usePrivy } from '@privy-io/react-auth';
export function ConnectWallet() {
const { ready, authenticated, login, logout, user } = usePrivy();
if (!ready) {
return <div>Loading...</div>;
}
if (authenticated) {
return (
<div className="wallet-connected">
<div className="wallet-info">
<span className="wallet-address">
{user?.wallet?.address.slice(0, 6)}...
{user?.wallet?.address.slice(-4)}
</span>
</div>
<button onClick={logout} className="btn-disconnect">
Disconnect
</button>
</div>
);
}
return (
<button onClick={login} className="btn-connect">
Connect Wallet
</button>
);
}contracts/NFT.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFTMinter is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
event NFTMinted(address indexed minter, uint256 indexed tokenId, string tokenURI);
constructor() ERC721("My NFT Collection", "MNFT") {}
function mintNFT(address recipient, string memory tokenURI)
public
returns (uint256)
{
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(recipient, newTokenId);
_setTokenURI(newTokenId, tokenURI);
emit NFTMinted(recipient, newTokenId, tokenURI);
return newTokenId;
}
function getTotalSupply() public view returns (uint256) {
return _tokenIds.current();
}
}src/components/MintNFT.tsx:
import { useState } from 'react';
import { useWalletClient } from 'wagmi';
import { parseEther } from 'viem';
import { uploadToIPFS } from '../lib/ipfs';
import { mintNFT } from '../lib/contract';
export function MintNFT() {
const { data: walletClient } = useWalletClient();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [image, setImage] = useState<File | null>(null);
const [minting, setMinting] = useState(false);
const [txHash, setTxHash] = useState('');
const handleMint = async (e: React.FormEvent) => {
e.preventDefault();
if (!walletClient || !image) return;
try {
setMinting(true);
// 1. Upload image to IPFS
console.log('Uploading image to IPFS...');
const imageUrl = await uploadToIPFS(image);
// 2. Create metadata
const metadata = {
name,
description,
image: imageUrl,
attributes: [],
};
// 3. Upload metadata to IPFS
console.log('Uploading metadata to IPFS...');
const metadataUrl = await uploadToIPFS(
new Blob([JSON.stringify(metadata)], { type: 'application/json' })
);
// 4. Mint NFT
console.log('Minting NFT...');
const hash = await mintNFT(walletClient, metadataUrl);
setTxHash(hash);
alert('NFT Minted Successfully!');
// Reset form
setName('');
setDescription('');
setImage(null);
} catch (error) {
console.error('Minting error:', error);
alert(error.message || 'Failed to mint NFT');
} finally {
setMinting(false);
}
};
return (
<div className="mint-container">
<h2>Mint Your NFT</h2>
<form onSubmit={handleMint}>
<div className="form-group">
<label>Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
placeholder="My Awesome NFT"
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
required
placeholder="Describe your NFT..."
/>
</div>
<div className="form-group">
<label>Image</label>
<input
type="file"
accept="image/*"
onChange={(e) => setImage(e.target.files?.[0] || null)}
required
/>
{image && (
<img
src={URL.createObjectURL(image)}
alt="Preview"
className="image-preview"
/>
)}
</div>
<button
type="submit"
disabled={minting || !walletClient}
className="btn-mint"
>
{minting ? 'Minting...' : 'Mint NFT'}
</button>
</form>
{txHash && (
<div className="success-message">
<p>Transaction Hash:</p>
<a
href={`https://mumbai.polygonscan.com/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{txHash.slice(0, 10)}...{txHash.slice(-8)}
</a>
</div>
)}
</div>
);
}src/components/NFTGallery.tsx:
import { useEffect, useState } from 'react';
import { usePublicClient } from 'wagmi';
import { NFTCard } from './NFTCard';
import { fetchNFTs } from '../lib/contract';
export function NFTGallery() {
const publicClient = usePublicClient();
const [nfts, setNfts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadNFTs() {
try {
const fetchedNFTs = await fetchNFTs(publicClient);
setNfts(fetchedNFTs);
} catch (error) {
console.error('Error fetching NFTs:', error);
} finally {
setLoading(false);
}
}
loadNFTs();
}, [publicClient]);
if (loading) {
return <div className="loading">Loading NFTs...</div>;
}
if (nfts.length === 0) {
return (
<div className="empty-state">
<p>No NFTs minted yet. Be the first to mint!</p>
</div>
);
}
return (
<div className="nft-gallery">
<h2>NFT Collection</h2>
<div className="nft-grid">
{nfts.map((nft) => (
<NFTCard key={nft.tokenId} nft={nft} />
))}
</div>
</div>
);
}- Go to https://faucet.polygon.technology
- Select "Mumbai"
- Paste your wallet address
- Click "Submit"
- Wait ~1 minute for test MATIC
npm run devOpen: http://localhost:5173
-
Click "Connect Wallet"
- Choose MetaMask (or any wallet)
- Approve the connection
- See your wallet address displayed
-
Mint an NFT
- Fill in name and description
- Upload an image
- Click "Mint NFT"
- Approve the transaction in MetaMask
- Wait for confirmation
-
View in Gallery
- Your NFT appears automatically!
- Click to see full details
-
View on Polygonscan
- Click the transaction link
- See it on the blockchain explorer
In Privy Dashboard:
- Add production URL:
https://your-app.vercel.app - Update chain to Polygon Mainnet (for production)
- Save changes
# In hardhat folder
npx hardhat run scripts/deploy.ts --network polygon
# Copy the contract address
# Update .env.production:
VITE_NFT_CONTRACT_ADDRESS=0x_new_mainnet_address# Build for production
npm run build
# Deploy
vercel --prodAdd environment variables in Vercel:
VITE_PRIVY_APP_IDVITE_ALCHEMY_API_KEYVITE_NFT_CONTRACT_ADDRESSVITE_CHAIN_ID=137(Polygon Mainnet)
Your dApp is live! 🎉
import { useBalance } from 'wagmi';
import { usePrivy } from '@privy-io/react-auth';
export function WalletBalance() {
const { user } = usePrivy();
const { data: balance } = useBalance({
address: user?.wallet?.address,
});
return (
<div className="balance">
<span>{balance?.formatted} {balance?.symbol}</span>
</div>
);
}import { usePublicClient } from 'wagmi';
export function TransactionHistory() {
const publicClient = usePublicClient();
const [transactions, setTransactions] = useState([]);
useEffect(() => {
async function fetchTxs() {
// Fetch transactions for user's address
const logs = await publicClient.getLogs({
address: contractAddress,
event: 'NFTMinted',
fromBlock: 'earliest',
});
setTransactions(logs);
}
fetchTxs();
}, []);
return (
<div className="tx-history">
{transactions.map((tx) => (
<div key={tx.transactionHash}>
<a href={`https://polygonscan.com/tx/${tx.transactionHash}`}>
{tx.transactionHash.slice(0, 10)}...
</a>
</div>
))}
</div>
);
}async function estimateGas() {
const gasEstimate = await publicClient.estimateContractGas({
address: contractAddress,
abi: contractABI,
functionName: 'mintNFT',
args: [userAddress, tokenURI],
});
const gasPrice = await publicClient.getGasPrice();
const gasCost = gasEstimate * gasPrice;
console.log(`Estimated gas cost: ${formatEther(gasCost)} MATIC`);
return gasCost;
}import { useSwitchNetwork } from 'wagmi';
export function NetworkSwitcher() {
const { chains, switchNetwork } = useSwitchNetwork();
return (
<select onChange={(e) => switchNetwork?.(Number(e.target.value))}>
{chains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.name}
</option>
))}
</select>
);
}Check .env:
# Make sure this is set
VITE_PRIVY_APP_ID=clp...Get test MATIC from faucet: https://faucet.polygon.technology
Re-run deployment:
cd hardhat
npx hardhat run scripts/deploy.ts --network mumbaiSwitch to Polygon Mumbai in MetaMask:
- Open MetaMask
- Click network dropdown
- Select "Polygon Mumbai" or add it manually
Try alternative IPFS service:
- Pinata: https://pinata.cloud
- NFT.Storage: https://nft.storage
- Web3.Storage: https://web3.storage
Add Features:
- NFT marketplace (buy/sell)
- Lazy minting (mint on purchase)
- Royalties for creators
- Batch minting
- NFT staking
- Token gating
Improve UX:
- Loading skeletons
- Transaction animations
- Error boundaries
- Mobile optimization
- Progressive Web App
Add Security:
- Rate limiting
- Signature verification
- Contract upgradability
- Pausable contracts
Scale:
- Layer 2 solutions (Polygon, Arbitrum)
- IPFS pinning service
- CDN for assets
- Caching layer
You built:
- ✅ Full Web3 dApp with React + TypeScript
- ✅ Privy wallet authentication (email, social, wallet)
- ✅ Smart contract on Polygon
- ✅ NFT minting functionality
- ✅ IPFS integration for metadata
- ✅ NFT gallery
- ✅ Deployed to production
From zero to Web3 in ~75 minutes!
Welcome to Web3 development! 🚀⛓️