About Novaport 🚀

💡 Inspiration

The DeFi user experience is broken. Managing a crypto portfolio requires:

  • Remembering 12-24 word seed phrases
  • Manually calculating rebalancing percentages in spreadsheets
  • Paying gas fees for every transaction
  • Understanding complex blockchain mechanics

Meanwhile, Web2 apps like Robinhood make portfolio management intuitive with simple sliders and one-click rebalancing. Why can't DeFi be that simple?

When I discovered Moonbeam's EVM compatibility with Polkadot's cross-chain capabilities, combined with account abstraction (ERC-4337), I saw an opportunity to bridge this UX gap. I wanted to prove that Web3 can be as easy as Web2 while maintaining true ownership of assets.

The physics-inspired "Laws of Motion" sliders came from a simple observation: portfolio allocation is a zero-sum game. If you increase one allocation, another must decrease. Instead of making users do mental math, why not make the UI enforce this constraint automatically, like Newton's laws govern physical motion?


🎓 What I Learned

1. Account Abstraction is a Game-Changer

Before this project, I'd only read about ERC-4337. Implementing it with Pimlico's infrastructure taught me:

  • Gas sponsorship eliminates the biggest UX barrier in crypto
  • Smart wallets enable batching multiple operations atomically
  • Social login via Privy makes onboarding seamless
  • User operations provide better UX than traditional transactions

The moment I saw my first gasless transaction execute on Moonbeam was magical. No more "you need ETH for gas" errors!

2. Token Decimal Handling is Critical

I learned this the hard way when a WBTC send tried to transfer 171 BTC instead of 0.0000024 BTC. The bug:

// WRONG: Hardcoded 18 decimals ❌
parseUnits("0.0000024", 18) // = 2,400,000,000,000

// CORRECT: Use actual decimals ✅
parseUnits("0.0000024", 8)  // = 240

Different tokens use different decimal places:

  • GLMR/ETH: 18 decimals (standard)
  • WBTC: 8 decimals (like Bitcoin)
  • USDC/USDT: 6 decimals

I now fetch decimals from Zapper's API and handle each token correctly. This taught me that DeFi requires precision - a single decimal place error can cost users millions.

3. Caching is Your Enemy in Real-Time DeFi

My portfolio was showing tokens from minutes ago! The culprits:

  • Next.js API route caching
  • Browser fetch() caching
  • External API caching

I learned to aggressively disable caching:

// API routes
headers: {
  'Cache-Control': 'no-store, no-cache, must-revalidate'
}

// Client fetches
fetch(url + `?t=${Date.now()}`, { cache: 'no-store' })

Lesson: In DeFi, stale data is wrong data. Always fetch fresh.

4. UI Constraints Prevent User Errors

My slider "Laws of Motion" aren't just clever - they're functional constraints:

LAW ONE: $\sum_{i=1}^{n} allocation_i = 100\%$

Ensures allocations always sum to 100%, preventing invalid portfolio states.

LAW TWO: $\Delta allocation_{moving} = -\Delta allocation_{last}$

Moving one slider causes the "last" slider to move opposite, maintaining the sum.

LAW THREE: $\text{if } |\text{unlocked}| = 1 \text{ then } \text{movement} = 0$

The last unlocked slider can't move (prevents breaking LAW ONE).

These constraints turned complex allocation math into simple, intuitive drag gestures. Users can't make mistakes because the UI won't let them.

5. Cross-Chain Swaps Require Aggregation

I integrated LI.FI for swap routing and learned:

  • Different DEXs have different liquidity pools
  • Prices vary across protocols
  • Aggregators find optimal routes across multiple DEXs
  • Slippage tolerance matters for price impact

LI.FI abstracts this complexity, but understanding it made me appreciate how fragmented DeFi liquidity really is.


🛠️ How I Built It

Architecture

┌─────────────┐
│   Browser   │
│  (React UI) │
└──────┬──────┘
       │
       ▼
┌─────────────────────┐
│   Next.js 15 App    │
│  ┌──────────────┐   │
│  │ Portfolio UI │   │
│  │   Sliders    │   │
│  │   Modals     │   │
│  └──────┬───────┘   │
│         │           │
│  ┌──────▼───────┐   │
│  │ API Routes   │   │
│  │ /portfolio   │   │
│  │ /swap-quote  │   │
│  └──────┬───────┘   │
└─────────┼───────────┘
          │
          ▼
    ┌─────────────┐
    │  External   │
    │    APIs     │
    ├─────────────┤
    │ Privy       │ ← Social login
    │ Pimlico     │ ← Gas sponsorship
    │ Zapper      │ ← Portfolio data
    │ LI.FI       │ ← Swap routing
    └─────────────┘
          │
          ▼
    ┌─────────────┐
    │  Moonbeam   │
    │  Blockchain │
    └─────────────┘

Tech Stack Deep Dive

Frontend:

  • Next.js 15 with Turbopack for fast dev builds
  • TypeScript for type safety (caught many bugs!)
  • Tailwind CSS v4 for rapid styling
  • Viem for Ethereum interactions (better than ethers.js!)

Authentication & Wallets:

  • Privy handles social login (Google, Twitter, Email)
  • Creates embedded wallets automatically
  • No seed phrases, no MetaMask required

Account Abstraction:

  • Pimlico provides ERC-4337 infrastructure
  • Paymaster sponsors all gas fees
  • Smart contract wallets enable batching
  • User operations instead of traditional txns

Data & Swaps:

  • Zapper API aggregates portfolio across protocols
  • Returns balances, prices, decimals, logos
  • LI.FI routes swaps across DEXs
  • Finds optimal paths with best prices

Blockchain:

  • Moonbeam - EVM-compatible Polkadot parachain
  • Full Ethereum compatibility
  • Native cross-chain messaging via XCM

Implementation Phases

Phase 1: Foundation

  • Set up Next.js app from Privy starter
  • Integrate Privy for social login
  • Create smart wallet with Pimlico
  • Basic UI layout

*Phase 2: Portfolio *

  • Integrate Zapper API for portfolio data
  • Display token balances and values
  • Calculate percentages
  • Handle token decimals correctly

*Phase 3: Sliders *

  • Implement slider UI
  • Add "Laws of Motion" logic
  • Add slider locking
  • Handle edge cases (1 unlocked slider, etc.)

Phase 4: Rebalancing

  • Integrate LI.FI for swap quotes
  • Build rebalancing algorithm
  • Batch multiple swaps
  • Execute with gas sponsorship

*Phase 5: Batch Send

  • Multi-token send UI
  • Native vs ERC-20 detection
  • MAX/percentage buttons
  • Proper decimal handling

*Phase 6: Polish

  • Add token search (40+ verified tokens)
  • Fix caching issues
  • Improve error handling
  • Add comprehensive logging
  • Write documentation

🚧 Challenges I Faced

Challenge 1: The WBTC Overflow Bug 😱

Problem: Batch send tried to transfer 171 BTC instead of 0.0000024 BTC

Root Cause: I hardcoded 18 decimals for all tokens:

decimals: 18 // ❌ WRONG for WBTC!

WBTC uses 8 decimals (like Bitcoin). When I called:

parseUnits("0.0000024", 18)
// Expected: 240 (with 8 decimals)
// Got: 2,400,000,000,000 (with 18 decimals!)

Solution:

  • Fetch real decimals from Zapper API
  • Store decimals per token in state
  • Use correct decimals in parseUnits()

Lesson: Never assume decimal places in DeFi!

Challenge 2: The Ghost Tokens 👻

Problem: Portfolio showed tokens from minutes ago

Root Cause: Multiple layers of caching:

  • Next.js API routes cached responses
  • Browser cached fetch() requests
  • Zapper API cached data
  • "Dust" tokens with $0 balance still showed

Solution:

// API: Force no-cache headers
return NextResponse.json(data, {
  headers: {
    'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0'
  }
});

// Client: Cache busting + no-store
fetch(`/api/portfolio?t=${Date.now()}`, {
  cache: 'no-store'
});

// Filter dust tokens
tokens.filter(t => t.balance > 0 && t.balanceUSD > 0.01)

Lesson: In real-time apps, be aggressive about cache control.

Challenge 3: Slider Logic Edge Cases 🎢

Problem: Two unlocked sliders couldn't both move

Root Cause: I calculated "last slider" BEFORE excluding the moving slider:

// WRONG ❌
const lastSlider = unlockedSliders[unlockedSliders.length - 1];
if (movingSlider === lastSlider) return; // Blocks bottom slider!

When moving the bottom slider, it thought IT was the last slider, so blocked it.

Solution: Exclude moving slider first:

// CORRECT ✅
const othersUnlocked = unlockedSliders.filter(i => i !== moving);
const lastSlider = othersUnlocked[othersUnlocked.length - 1];

Now both sliders can move - each becomes the "last" when the other moves.

Lesson: Off-by-one errors are everywhere. Test edge cases!

Challenge 4: Silent Batch Send Failures 🤫

Problem: Batch send failed with no error messages

Root Cause: No try/catch in the execution callback:

// WRONG ❌
onExecute={async (calls) => {
  await executeBatchCalls(calls); // If this fails, no one knows!
}}

Solution: Comprehensive error handling + logging:

// CORRECT ✅
onExecute={async (calls) => {
  try {
    console.log("🚀 Executing batch send...", calls);
    await executeBatchCalls(calls);
    console.log("✅ Success!");
  } catch (err) {
    console.error("❌ Failed:", err);
    throw err; // Re-throw so modal can display
  }
}}

Added logs at every step:

  • 🔍 Validation
  • 💸 Each send item
  • → Native vs ERC-20 detection
  • 🚀 Execution
  • ✅ Success or ❌ Error

Lesson: In Web3, transactions can fail in many ways. Log everything!

Challenge 5: Token Logo Display 🖼️

Problem: Token logos wouldn't load from CoinGecko/Moonbeam

Root Cause: Next.js Image component requires allowed domains:

// next.config.ts only had:
remotePatterns: [{ hostname: 'storage.googleapis.com' }]

But logos came from assets.coingecko.com and moonbeam.network.

Solution: I switched to regular <img> tags with fallback:

<img 
  src={token.logo} 
  onError={() => setImageError(true)}
/>
{imageError && <div className="fallback-icon">{symbol[0]}</div>}

Lesson: External image sources need proper configuration.

🎯 What I'm Proud Of

  1. The Sliders Work Perfectly - My "Laws of Motion" make portfolio rebalancing intuitive. No spreadsheets needed!

  2. Zero Gas Fees - Users never pay gas. Pimlico's paymaster sponsors everything. This is the future of Web3 UX.

  3. True Social Login - No seed phrases. Login with Google. Your crypto wallet is as easy to access as Gmail.

  4. Proper Decimal Handling - After the WBTC bug, I handle every token correctly. 8 decimals for BTC, 6 for USDC, 18 for ETH.

  5. Comprehensive Error Handling - Every operation logs detailed info. When something fails, you know exactly why.

  6. Real-Time Data - No stale cache. Portfolio always shows current balances.

  7. Batch Operations - Send multiple tokens in one click. Rebalance your entire portfolio atomically.

  8. Proper Decimal Handling - After the WBTC bug, I handle every token correctly. 8 decimals for BTC, 6 for USDC, 18 for ETH.

Built With

  • lifi
  • nextjs
  • pimlico
  • privy
  • remix
Share this project:

Updates