Back to blog

Building a Web3 Portfolio: Blockchain Integration for Developers

February 28, 2025 (1y ago)

Web3 is more than just buzzwords—it's becoming a legitimate career path for developers. Here's how I added blockchain features to my portfolio and what I learned along the way.

Why Web3 in a Portfolio?

Traditional portfolios show what you've built. Web3 portfolios can prove what you've built through on-chain verification.

Key Benefits

  • Immutable proof of your work and achievements
  • Smart contract skills demonstration
  • Cutting-edge technology showcase
  • Decentralized credentials that can't be faked

Getting Started: The Web3 Stack

Essential Technologies

// Frontend integration
import { ethers } from 'ethers';
import { useAccount, useConnect } from 'wagmi';

// Smart contract interaction
const contract = new ethers.Contract(address, abi, signer);
const result = await contract.getPortfolioItems();

Development Environment

  • Hardhat for local development
  • MetaMask for wallet connection
  • IPFS for decentralized storage
  • Etherscan for contract verification

Project 1: On-Coin Resume

Smart Contract Design

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract OnChainResume {
    struct Experience {
        string company;
        string role;
        uint256 startDate;
        uint256 endDate;
        bool verified;
    }
    
    mapping(address => Experience[]) public experiences;
    mapping(address => bool) public verifiedDevelopers;
    
    function addExperience(
        string memory _company,
        string memory _role,
        uint256 _startDate,
        uint256 _endDate
    ) public {
        experiences[msg.sender].push(Experience({
            company: _company,
            role: _role,
            startDate: _startDate,
            endDate: _endDate,
            verified: false
        }));
    }
    
    function verifyExperience(address _developer, uint256 _index) public {
        // Only verified companies can verify
        require(verifiedCompanies[msg.sender], "Not authorized");
        experiences[_developer][_index].verified = true;
    }
}

Frontend Integration

function OnChainResume() {
  const { address } = useAccount();
  const [experiences, setExperiences] = useState([]);
  
  const loadExperiences = async () => {
    if (!address) return;
    
    const contract = new ethers.Contract(
      RESUME_CONTRACT_ADDRESS,
      RESUME_ABI,
      provider
    );
    
    const userExperiences = await contract.getExperiences(address);
    setExperiences(userExperiences);
  };
  
  return (
    <div className="on-chain-resume">
      {experiences.map((exp, index) => (
        <div key={index} className={`experience ${exp.verified ? 'verified' : ''}`}>
          <h3>{exp.role}</h3>
          <p>{exp.company}</p>
          {exp.verified && <span className="verified-badge">✓ Verified</span>}
        </div>
      ))}
    </div>
  );
}

Project 2: NFT Project Gallery

Creating Project NFTs

// Mint NFTs for completed projects
async function mintProjectNFT(projectData) {
  const metadata = {
    name: projectData.title,
    description: projectData.description,
    image: projectData.imageUrl,
    attributes: [
      { trait_type: "Technology", value: projectData.tech },
      { trait_type: "Year", value: projectData.year },
      { trait_type: "Type", value: projectData.type }
    ],
    external_url: projectData.liveUrl
  };
  
  // Upload to IPFS
  const ipfsHash = await uploadToIPFS(metadata);
  
  // Mint NFT
  const tx = await contract.mintProjectNFT(ipfsHash);
  await tx.wait();
  
  return tx.hash;
}

Gallery Display

function NFTGallery() {
  const [nfts, setNfts] = useState([]);
  
  useEffect(() => {
    const loadNFTs = async () => {
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const contract = new ethers.Contract(NFT_ADDRESS, NFT_ABI, provider);
      
      const ownerNFTs = await contract.getOwnerNFTs(address);
      const nftMetadata = await Promise.all(
        ownerNFTs.map(async (tokenId) => {
          const tokenURI = await contract.tokenURI(tokenId);
          return fetchIPFSMetadata(tokenURI);
        })
      );
      
      setNfts(nftMetadata);
    };
    
    loadNFTs();
  }, [address]);
  
  return (
    <div className="nft-gallery">
      {nfts.map((nft) => (
        <div key={nft.tokenId} className="project-nft">
          <img src={nft.image} alt={nft.name} />
          <h3>{nft.name}</h3>
          <div className="attributes">
            {nft.attributes.map(attr => (
              <span key={attr.trait_type} className="attribute">
                {attr.trait_type}: {attr.value}
              </span>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

Project 3: Decentralized Blog

Blockchain-Powered Comments

contract BlogComments {
    struct Comment {
        address author;
        string content;
        uint256 timestamp;
        uint256 likes;
        mapping(address => bool) hasLiked;
    }
    
    mapping(string => Comment[]) public postComments;
    mapping(address => uint256) public userReputation;
    
    function addComment(string memory postHash, string memory content) public {
        Comment memory newComment = Comment({
            author: msg.sender,
            content: content,
            timestamp: block.timestamp,
            likes: 0
        });
        
        postComments[postHash].push(newComment);
        userReputation[msg.sender] += 1;
    }
    
    function likeComment(string memory postHash, uint256 commentIndex) public {
        Comment storage comment = postComments[postHash][commentIndex];
        require(!comment.hasLiked[msg.sender], "Already liked");
        
        comment.hasLiked[msg.sender] = true;
        comment.likes++;
        userReputation[comment.author] += 1;
    }
}

Integration with MDX

function DecentralizedComments({ postSlug }) {
  const [comments, setComments] = useState([]);
  const [newComment, setNewComment] = useState('');
  
  const addComment = async () => {
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    const signer = await provider.getSigner();
    const contract = new ethers.Contract(BLOG_CONTRACT, BLOG_ABI, signer);
    
    const tx = await contract.addComment(postSlug, newComment);
    await tx.wait();
    
    setNewComment('');
    loadComments();
  };
  
  return (
    <div className="decentralized-comments">
      <div className="comment-form">
        <textarea
          value={newComment}
          onChange={(e) => setNewComment(e.target.value)}
          placeholder="Add a comment..."
        />
        <button onClick={addComment}>Post Comment</button>
      </div>
      
      <div className="comments-list">
        {comments.map((comment, index) => (
          <div key={index} className="comment">
            <div className="comment-header">
              <span className="author">{formatAddress(comment.author)}</span>
              <span className="timestamp">{formatDate(comment.timestamp)}</span>
            </div>
            <p className="content">{comment.content}</p>
            <div className="comment-actions">
              <span>{comment.likes} likes</span>
              <button onClick={() => likeComment(index)}>Like</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Security Considerations

Smart Contract Security

// Always implement access controls
modifier onlyOwner() {
    require(msg.sender == owner, "Not authorized");
    _;
}

// Prevent reentrancy attacks
bool private locked;
modifier noReentrant() {
    require(!locked, "Reentrant call");
    locked = true;
    _;
    locked = false;
}

// Use SafeMath for arithmetic (or Solidity 0.8+)
using SafeMath for uint256;

Frontend Security

// Never expose private keys
const PRIVATE_KEY = process.env.NEXT_PUBLIC_PRIVATE_KEY; // ❌ NEVER DO THIS

// Use environment variables for sensitive data
const PRIVATE_KEY = process.env.PRIVATE_KEY; // ✅ Correct

// Validate user inputs
function validateInput(input) {
  if (input.length > 1000) throw new Error("Input too long");
  if (input.includes('<script>')) throw new Error("Invalid characters");
  return input;
}

Gas Optimization Tips

Efficient Contract Design

// Pack structs to save gas
struct PackedUser {
    uint128 userId;      // Instead of uint256
    uint64 reputation;   // Smaller types where possible
    bool isActive;       // Booleans are cheap
    uint32 joinDate;     // Timestamp fits in uint32
}

// Use events instead of storage for historical data
event ProjectCompleted(
    address indexed developer,
    string indexed projectHash,
    uint256 timestamp
);

// Batch operations when possible
function batchVerifyProjects(address[] calldata developers, uint256[] calldata projectIds) 
    external onlyOwner {
    require(developers.length == projectIds.length, "Array length mismatch");
    
    for (uint256 i = 0; i < developers.length; i++) {
        projects[developers[i]][projectIds[i]].verified = true;
    }
}

Deployment Strategy

Testnet First

// Deploy to testnet for testing
async function deployToTestnet() {
  const contract = await ethers.getContractFactory("Web3Portfolio");
  const deployed = await contract.deploy();
  
  console.log("Testnet deployment:", deployed.address);
  
  // Verify on Etherscan
  await hre.run("verify:verify", {
    address: deployed.address,
    constructorArguments: []
  });
}

Mainnet Considerations

  • Gas costs can be significant for complex contracts
  • Immutable deployments - test thoroughly
  • Upgrade patterns - consider proxy contracts for future updates

The User Experience

Wallet Connection

function WalletConnector() {
  const { connect, isConnected, address } = useConnect();
  
  if (!isConnected) {
    return (
      <button onClick={() => connect()}>
        Connect Wallet
      </button>
    );
  }
  
  return (
    <div className="wallet-connected">
      <span>{formatAddress(address)}</span>
      <button onClick={disconnect}>Disconnect</button>
    </div>
  );
}

Network Switching

async function switchToPolygon() {
  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: '0x89' }], // Polygon
    });
  } catch (error) {
    // Network not added, add it
    await window.ethereum.request({
      method: 'wallet_addEthereumChain',
      params: [{
        chainId: '0x89',
        chainName: 'Polygon',
        rpcUrls: ['https://polygon-rpc.com/'],
      }],
    });
  }
}

Measuring Success

On-Chain Metrics

  • NFTs minted for projects
  • Verification status of experience
  • Community engagement through likes/comments
  • Gas usage optimization

Off-Chain Metrics

  • Portfolio visits from Web3 enthusiasts
  • GitHub stars on smart contracts
  • Community feedback and engagement
  • Job inquiries from Web3 companies

Challenges I Faced

Technical Challenges

  • Gas optimization for complex contracts
  • IPFS reliability for metadata storage
  • Cross-chain compatibility issues
  • Wallet integration across different providers

Educational Challenges

  • Explaining Web3 concepts to traditional developers
  • Demonstrating value beyond the hype
  • Security concerns from potential employers
  • Regulatory uncertainty in the space

Future Enhancements

What's Next for My Web3 Portfolio

  • Cross-chain support for multiple blockchains
  • DAO membership verification
  • DeFi integration for token-gated content
  • Metaverse integration for 3D portfolio experiences

Emerging Technologies

  • Account abstraction for better UX
  • Layer 2 solutions for lower costs
  • Zero-knowledge proofs for privacy
  • AI + Web3 combinations

Conclusion

Building a Web3 portfolio isn't just about adding blockchain—it's about demonstrating your ability to work with cutting-edge technology and think differently about problems.

The Web3 space moves fast, but the fundamentals stay the same: build useful things, solve real problems, and never stop learning.

Whether you're deep into Web3 or just getting started, adding blockchain features to your portfolio can open doors you didn't even know existed.


What Web3 features have you experimented with? Share your experiences and let's learn together!

Share this post