Skip to main content

Overview

These recipes show specific allocation management patterns you can implement. Each pattern includes the allocation setup, frontend handling, and contract enforcement code.

Reserved + Overflow Pool

Use case: Give strategic investors guaranteed allocations, then open remaining tokens to public.

Setup

// Phase 1: Strategic investors with reserved allocations
const strategicAllocations = [
  { wallet: "0xabc...", reserved: "100000000000", max: "100000000000" }, // $100k reserved (full)
  { wallet: "0xdef...", reserved: "50000000000",  max: "75000000000" },  // $50k reserved, $75k max
  { wallet: "0x123...", reserved: "25000000000",  max: "50000000000" }   // $25k reserved, $50k max
];

// Total reserved: $175k
// If only $150k is purchased by strategic investors, $25k flows to public pool

// Phase 2: Public gets remaining + overflow
const publicPool = {
  totalSaleCap: "1000000000000", // $1M total sale
  reservedForStrategic: "175000000000", // $175k reserved
  availableForPublic: "825000000000" + overflow // $825k + any strategic undersubscription
};

Frontend Implementation

function ReservedOverflowSale() {
  const [phase, setPhase] = useState<"strategic" | "public">("strategic");
  const [overflowAmount, setOverflowAmount] = useState(0n);
  
  useEffect(() => {
    // Check current phase and calculate overflow
    const checkPhaseAndOverflow = async () => {
      const currentPhase = await getCurrentSalePhase();
      setPhase(currentPhase);
      
      if (currentPhase === "public") {
        // Calculate how much strategic investors didn't use
        const totalReserved = 175000000000n; // $175k
        const strategicPurchased = await getStrategicPurchaseTotal();
        const overflow = totalReserved - strategicPurchased;
        setOverflowAmount(overflow);
      }
    };
    
    checkPhaseAndOverflow();
  }, []);
  
  if (phase === "strategic") {
    return <StrategicPhase />;
  }
  
  return <PublicPhaseWithOverflow overflowAmount={overflowAmount} />;
}

function PublicPhaseWithOverflow({ overflowAmount }) {
  const basePublicPool = 825000000000n; // $825k base
  const totalPublicPool = basePublicPool + overflowAmount;
  
  return (
    <div>
      <div className="mb-4 p-4 bg-blue-50 rounded-lg">
        <h3 className="text-blue-800 font-medium">Public Sale Open</h3>
        <div className="mt-2 space-y-1 text-sm">
          <div className="flex justify-between">
            <span>Base Public Pool:</span>
            <span>${formatUSDC(basePublicPool)}</span>
          </div>
          {overflowAmount > 0n && (
            <div className="flex justify-between text-green-600">
              <span>Overflow from Strategic:</span>
              <span>+${formatUSDC(overflowAmount)}</span>
            </div>
          )}
          <div className="flex justify-between font-medium border-t pt-1">
            <span>Total Available:</span>
            <span>${formatUSDC(totalPublicPool)}</span>
          </div>
        </div>
      </div>
      
      <FCFSPurchaseFlow totalPool={totalPublicPool} />
    </div>
  );
}

Contract Pattern

contract ReservedOverflowSale is ExampleSale {
    uint256 public constant TOTAL_SALE_CAP = 1_000_000_000_000; // $1M
    uint256 public constant STRATEGIC_RESERVED = 175_000_000_000; // $175k
    
    uint256 public strategicPurchased = 0;
    uint256 public publicPurchased = 0;
    
    enum Phase { Strategic, Public }
    Phase public currentPhase = Phase.Strategic;

    error StrategicPhaseEnded();
    error ExceedsPublicPool(uint256 attempted, uint256 available);
    error AlreadyInPublicPhase();

    constructor(bytes16 _saleUUID, address _signer) 
        ExampleSale(_saleUUID, _signer) {}
    
    function purchase(
        uint256 amount,
        PurchasePermitWithAllocation calldata purchasePermit,
        bytes calldata purchasePermitSignature
    ) external override {
        _validatePurchasePermit(purchasePermit, purchasePermitSignature);
        
        if (currentPhase == Phase.Strategic) {
            _handleStrategicPurchase(amount, purchasePermit);
        } else {
            _handlePublicPurchase(amount, purchasePermit);
        }
    }

    function _handleStrategicPurchase(uint256 amount, PurchasePermitWithAllocation calldata purchasePermit) internal {
        uint256 newTotalAmount = amountByAddress[msg.sender] + amount;

        // Standard permit validation
        if (newTotalAmount < purchasePermit.minAmount) {
            revert AmountBelowMinimum(newTotalAmount, purchasePermit.minAmount);
        }
        if (newTotalAmount > purchasePermit.maxAmount) {
            revert AmountExceedsMaximum(newTotalAmount, purchasePermit.maxAmount);
        }

        _trackEntity(purchasePermit.permit.entityID, msg.sender);
        amountByAddress[msg.sender] = newTotalAmount;
        strategicPurchased += amount;

        emit Purchased(msg.sender, purchasePermit.permit.entityID, amount, newTotalAmount);
    }
    
    function _handlePublicPurchase(uint256 amount, PurchasePermitWithAllocation calldata purchasePermit) internal {
        // Calculate available public pool (base + strategic overflow)
        uint256 strategicOverflow = STRATEGIC_RESERVED - strategicPurchased;
        uint256 basePublicPool = TOTAL_SALE_CAP - STRATEGIC_RESERVED;
        uint256 totalPublicAvailable = basePublicPool + strategicOverflow;
        
        if (publicPurchased + amount > totalPublicAvailable) {
            revert ExceedsPublicPool(publicPurchased + amount, totalPublicAvailable - publicPurchased);
        }

        uint256 newTotalAmount = amountByAddress[msg.sender] + amount;

        // Standard permit validation
        if (newTotalAmount < purchasePermit.minAmount) {
            revert AmountBelowMinimum(newTotalAmount, purchasePermit.minAmount);
        }
        if (newTotalAmount > purchasePermit.maxAmount) {
            revert AmountExceedsMaximum(newTotalAmount, purchasePermit.maxAmount);
        }

        _trackEntity(purchasePermit.permit.entityID, msg.sender);
        amountByAddress[msg.sender] = newTotalAmount;
        publicPurchased += amount;

        emit Purchased(msg.sender, purchasePermit.permit.entityID, amount, newTotalAmount);
    }
    
    function switchToPublicPhase() external {
        if (currentPhase != Phase.Strategic) {
            revert AlreadyInPublicPhase();
        }
        currentPhase = Phase.Public;
    }
    
    function getAvailablePublicPool() external view returns (uint256) {
        if (currentPhase == Phase.Strategic) return 0;
        
        uint256 strategicOverflow = STRATEGIC_RESERVED - strategicPurchased;
        uint256 basePublicPool = TOTAL_SALE_CAP - STRATEGIC_RESERVED;
        return basePublicPool + strategicOverflow - publicPurchased;
    }
}

Dynamic Allocation Increases

Use case: Reward active participants by increasing their allocations during the sale.

Setup

// Initial conservative allocations
const initialAllocations = [
  { wallet: "0x111...", max: "5000000000" },   // $5k initial
  { wallet: "0x222...", max: "10000000000" },  // $10k initial
  { wallet: "0x333...", max: "2500000000" }    // $2.5k initial
];

// Triggers for allocation increases
const increaseTriggers = {
  earlyParticipant: { 
    condition: "purchase_within_first_hour",
    increase: "5000000000" // +$5k bonus
  },
  largeVolume: {
    condition: "purchase_over_threshold",
    threshold: "2500000000", // $2.5k+
    increase: "7500000000"   // +$7.5k bonus
  },
  referral: {
    condition: "successful_referral",
    increase: "2500000000"   // +$2.5k per referral
  }
};

Frontend Implementation

function DynamicAllocationSale() {
  const [currentAllocation, setCurrentAllocation] = useState(null);
  const [allocationHistory, setAllocationHistory] = useState([]);
  const [eligibleBonuses, setEligibleBonuses] = useState([]);
  
  useEffect(() => {
    // Fetch current allocation and check for bonuses
    const checkAllocationStatus = async () => {
      const allocation = await sonarClient.fetchAllocation(saleUUID, walletAddress);
      setCurrentAllocation(allocation);
      
      // Check if user is eligible for bonuses
      const bonuses = await checkEligibleBonuses(walletAddress);
      setEligibleBonuses(bonuses);
      
      // Fetch allocation history
      const history = await getAllocationHistory(walletAddress);
      setAllocationHistory(history);
    };
    
    checkAllocationStatus();
  }, []);
  
  return (
    <div className="space-y-4">
      {/* Current allocation display */}
      <div className="p-4 bg-green-50 rounded-lg">
        <h3 className="text-green-800 font-medium">Your Current Allocation</h3>
        <p className="text-2xl font-bold text-green-700">
          ${formatUSDC(currentAllocation?.MaxAmountUSD)}
        </p>
      </div>
      
      {/* Allocation increase opportunities */}
      {eligibleBonuses.length > 0 && (
        <AllocationBonusPanel bonuses={eligibleBonuses} />
      )}
      
      {/* Allocation history */}
      <AllocationHistoryPanel history={allocationHistory} />
      
      <PurchaseForm maxAmount={currentAllocation?.MaxAmountUSD} />
    </div>
  );
}

function AllocationBonusPanel({ bonuses }) {
  return (
    <div className="p-4 bg-purple-50 rounded-lg">
      <h3 className="text-purple-800 font-medium">🎉 Allocation Bonuses Available!</h3>
      <div className="mt-2 space-y-2">
        {bonuses.map((bonus, index) => (
          <div key={index} className="flex justify-between items-center">
            <span className="text-purple-700">{bonus.description}</span>
            <span className="font-medium text-purple-800">
              +${formatUSDC(bonus.amount)}
            </span>
          </div>
        ))}
      </div>
      <button 
        className="mt-3 px-4 py-2 bg-purple-600 text-white rounded"
        onClick={claimAllocationBonuses}
      >
        Claim Bonuses
      </button>
    </div>
  );
}

function AllocationHistoryPanel({ history }) {
  if (history.length <= 1) return null;
  
  return (
    <div className="p-4 bg-gray-50 rounded-lg">
      <h3 className="text-gray-800 font-medium">Allocation History</h3>
      <div className="mt-2 space-y-1">
        {history.map((entry, index) => (
          <div key={index} className="flex justify-between text-sm">
            <span className="text-gray-600">{entry.timestamp}</span>
            <span className="text-gray-700">{entry.reason}</span>
            <span className="font-medium text-green-600">
              ${formatUSDC(entry.newMax)}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

Backend Logic (Conceptual)

// API endpoint to handle allocation increases
async function updateAllocation(walletAddress: string, reason: string, increase: string) {
  const currentAllocation = await getAllocation(walletAddress);
  const newMaxAmount = (BigInt(currentAllocation.MaxAmountUSD) + BigInt(increase)).toString();
  
  // Update allocation in Sonar dashboard
  await sonarAdmin.updateAllocation({
    walletAddress,
    maxAmount: newMaxAmount,
    reason: reason // For audit trail
  });
  
  // Log the change
  await logAllocationChange({
    walletAddress,
    oldMax: currentAllocation.MaxAmountUSD,
    newMax: newMaxAmount,
    reason,
    timestamp: new Date().toISOString()
  });
  
  return { success: true, newMaxAmount };
}

// Automatic triggers
async function checkAndApplyBonuses(walletAddress: string, purchaseAmount: string) {
  const purchaseTime = Date.now();
  const saleStartTime = await getSaleStartTime();
  
  // Early participant bonus
  if (purchaseTime - saleStartTime < 3600000) { // Within 1 hour
    await updateAllocation(walletAddress, "Early participant bonus", "5000000000");
  }
  
  // Large volume bonus
  if (BigInt(purchaseAmount) >= BigInt("2500000000")) { // $2.5k+
    await updateAllocation(walletAddress, "Large volume bonus", "7500000000");
  }
  
  // Check referral bonuses
  const referrals = await getSuccessfulReferrals(walletAddress);
  if (referrals.length > 0) {
    const referralBonus = (BigInt("2500000000") * BigInt(referrals.length)).toString();
    await updateAllocation(walletAddress, `Referral bonus (${referrals.length} referrals)`, referralBonus);
  }
}

Graduated Allocation Tiers

Use case: Different allocation sizes based on investor categories or participation levels.

Setup

// Tier definitions
const allocationTiers = {
  whale: {
    minPurchase: "100000000000", // $100k minimum
    maxAllocation: "1000000000000", // $1M maximum
    reserved: "100000000000", // $100k reserved
    description: "Institutional/Whale Tier"
  },
  strategic: {
    minPurchase: "25000000000", // $25k minimum
    maxAllocation: "100000000000", // $100k maximum
    reserved: "25000000000", // $25k reserved
    description: "Strategic Investor Tier"
  },
  premium: {
    minPurchase: "5000000000", // $5k minimum
    maxAllocation: "25000000000", // $25k maximum
    reserved: "0", // No guaranteed allocation
    description: "Premium Tier"
  },
  standard: {
    minPurchase: "1000000000", // $1k minimum
    maxAllocation: "5000000000", // $5k maximum
    reserved: "0", // No guaranteed allocation
    description: "Standard Tier"
  }
};

// Assign tiers to wallets
const tierAssignments = [
  { wallet: "0xaaa...", tier: "whale" },
  { wallet: "0xbbb...", tier: "strategic" },
  { wallet: "0xccc...", tier: "premium" },
  { wallet: "0xddd...", tier: "standard" }
];

Frontend Implementation

function TieredAllocationSale() {
  const [userTier, setUserTier] = useState(null);
  const [allocation, setAllocation] = useState(null);
  const [tierInfo, setTierInfo] = useState(null);
  
  useEffect(() => {
    const loadTierInfo = async () => {
      // Fetch user's tier and allocation
      const [tier, alloc] = await Promise.all([
        getUserTier(walletAddress),
        sonarClient.fetchAllocation(saleUUID, walletAddress)
      ]);
      
      setUserTier(tier);
      setAllocation(alloc);
      setTierInfo(allocationTiers[tier]);
    };
    
    loadTierInfo();
  }, []);
  
  if (!userTier || !tierInfo) {
    return <div>Loading tier information...</div>;
  }
  
  return (
    <div className="space-y-4">
      {/* Tier badge */}
      <TierBadge tier={userTier} tierInfo={tierInfo} />
      
      {/* Allocation details */}
      <TierAllocationDisplay allocation={allocation} tierInfo={tierInfo} />
      
      {/* Purchase form with tier-specific constraints */}
      <TierPurchaseForm 
        allocation={allocation} 
        tierInfo={tierInfo}
        tier={userTier}
      />
      
      {/* Tier comparison (help users understand their tier) */}
      <TierComparisonPanel currentTier={userTier} />
    </div>
  );
}

function TierBadge({ tier, tierInfo }) {
  const tierColors = {
    whale: "bg-purple-100 text-purple-800 border-purple-200",
    strategic: "bg-blue-100 text-blue-800 border-blue-200", 
    premium: "bg-green-100 text-green-800 border-green-200",
    standard: "bg-gray-100 text-gray-800 border-gray-200"
  };
  
  const tierEmojis = {
    whale: "🐋",
    strategic: "⭐", 
    premium: "💎",
    standard: "🎯"
  };
  
  return (
    <div className={`inline-flex items-center px-3 py-1 rounded-full border ${tierColors[tier]}`}>
      <span className="mr-2">{tierEmojis[tier]}</span>
      <span className="font-medium">{tierInfo.description}</span>
    </div>
  );
}

function TierAllocationDisplay({ allocation, tierInfo }) {
  return (
    <div className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg">
      <h3 className="font-medium text-gray-800 mb-3">Your Tier Allocation</h3>
      
      <div className="grid grid-cols-2 gap-4">
        <div>
          <p className="text-sm text-gray-600">Maximum Allocation</p>
          <p className="text-xl font-bold text-gray-800">
            ${formatUSDC(allocation.MaxAmountUSD)}
          </p>
        </div>
        
        <div>
          <p className="text-sm text-gray-600">Minimum Purchase</p>
          <p className="text-xl font-bold text-gray-800">
            ${formatUSDC(tierInfo.minPurchase)}
          </p>
        </div>
        
        {allocation.ReservedAmountUSD !== "0" && (
          <div className="col-span-2">
            <p className="text-sm text-gray-600">Reserved (Guaranteed)</p>
            <p className="text-lg font-semibold text-green-700">
              ${formatUSDC(allocation.ReservedAmountUSD)}
            </p>
          </div>
        )}
      </div>
    </div>
  );
}

function TierPurchaseForm({ allocation, tierInfo, tier }) {
  const [purchaseAmount, setPurchaseAmount] = useState("");
  
  // Suggest tier-appropriate amounts
  const suggestedAmounts = {
    whale: ["100000", "250000", "500000", "1000000"],
    strategic: ["25000", "50000", "75000", "100000"],
    premium: ["5000", "10000", "15000", "25000"],
    standard: ["1000", "2500", "3500", "5000"]
  };
  
  return (
    <div className="space-y-4">
      <div>
        <label className="block text-sm font-medium mb-2">
          Purchase Amount (USD)
        </label>
        
        {/* Tier-specific quick amounts */}
        <div className="flex flex-wrap gap-2 mb-3">
          {suggestedAmounts[tier].map(amount => (
            <button
              key={amount}
              onClick={() => setPurchaseAmount(amount)}
              className="px-3 py-1 text-sm bg-gray-100 rounded hover:bg-gray-200"
            >
              ${formatUSDC(amount + "000000")}
            </button>
          ))}
        </div>
        
        <input
          type="number"
          value={purchaseAmount}
          onChange={(e) => setPurchaseAmount(e.target.value)}
          min={formatUSDC(tierInfo.minPurchase)}
          max={formatUSDC(allocation.MaxAmountUSD)}
          className="w-full px-3 py-2 border rounded"
        />
      </div>
      
      <button className="w-full py-3 bg-blue-600 text-white rounded font-medium">
        Purchase Tokens
      </button>
    </div>
  );
}

function TierComparisonPanel({ currentTier }) {
  return (
    <div className="p-4 bg-gray-50 rounded-lg">
      <h4 className="font-medium mb-3">Tier Comparison</h4>
      <div className="overflow-x-auto">
        <table className="w-full text-sm">
          <thead>
            <tr className="border-b">
              <th className="text-left py-2">Tier</th>
              <th className="text-right py-2">Min Purchase</th>
              <th className="text-right py-2">Max Allocation</th>
              <th className="text-right py-2">Reserved</th>
            </tr>
          </thead>
          <tbody>
            {Object.entries(allocationTiers).map(([tierName, info]) => (
              <tr 
                key={tierName}
                className={`border-b ${tierName === currentTier ? 'bg-blue-50 font-medium' : ''}`}
              >
                <td className="py-2">{info.description}</td>
                <td className="text-right py-2">${formatUSDC(info.minPurchase)}</td>
                <td className="text-right py-2">${formatUSDC(info.maxAllocation)}</td>
                <td className="text-right py-2">
                  {info.reserved === "0" ? "None" : `$${formatUSDC(info.reserved)}`}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

Allocation Transfer/Marketplace

Use case: Allow allocation holders to transfer unused allocations to other eligible participants.

Setup

// Enable allocation transfers in sale configuration
const transferConfig = {
  allowAllocationTransfers: true,
  transferFeePercentage: 250, // 2.5% transfer fee
  minTransferAmount: "1000000000", // $1k minimum
  transferDeadline: "2024-12-31T23:59:59Z", // Transfers must complete before sale ends
  requiresApproval: false // Auto-approve transfers vs manual review
};

Frontend Implementation

function AllocationMarketplace() {
  const [userAllocation, setUserAllocation] = useState(null);
  const [availableTransfers, setAvailableTransfers] = useState([]);
  const [showTransferModal, setShowTransferModal] = useState(false);
  
  useEffect(() => {
    const loadMarketplace = async () => {
      const [allocation, transfers] = await Promise.all([
        sonarClient.fetchAllocation(saleUUID, walletAddress),
        fetchAvailableAllocationTransfers(saleUUID)
      ]);
      
      setUserAllocation(allocation);
      setAvailableTransfers(transfers);
    };
    
    loadMarketplace();
  }, []);
  
  return (
    <div className="space-y-6">
      {/* User's allocation status */}
      <UserAllocationPanel 
        allocation={userAllocation}
        onCreateTransfer={() => setShowTransferModal(true)}
      />
      
      {/* Available allocations to purchase */}
      <AvailableTransfersPanel transfers={availableTransfers} />
      
      {/* Transfer creation modal */}
      {showTransferModal && (
        <CreateTransferModal 
          allocation={userAllocation}
          onClose={() => setShowTransferModal(false)}
        />
      )}
    </div>
  );
}

function UserAllocationPanel({ allocation, onCreateTransfer }) {
  const remainingAllocation = BigInt(allocation.MaxAmountUSD) - getCurrentPurchased();
  
  return (
    <div className="p-4 bg-blue-50 rounded-lg">
      <h3 className="text-blue-800 font-medium mb-3">Your Allocation</h3>
      
      <div className="grid grid-cols-2 gap-4 mb-4">
        <div>
          <p className="text-sm text-blue-600">Total Allocation</p>
          <p className="text-lg font-semibold">${formatUSDC(allocation.MaxAmountUSD)}</p>
        </div>
        <div>
          <p className="text-sm text-blue-600">Remaining</p>
          <p className="text-lg font-semibold">${formatUSDC(remainingAllocation)}</p>
        </div>
      </div>
      
      {remainingAllocation > 0n && (
        <div className="flex gap-2">
          <button className="px-4 py-2 bg-blue-600 text-white rounded">
            Purchase Tokens
          </button>
          <button 
            onClick={onCreateTransfer}
            className="px-4 py-2 bg-gray-600 text-white rounded"
          >
            Transfer Allocation
          </button>
        </div>
      )}
    </div>
  );
}

function AvailableTransfersPanel({ transfers }) {
  return (
    <div className="space-y-4">
      <h3 className="font-medium">Available Allocation Transfers</h3>
      
      {transfers.length === 0 ? (
        <div className="p-8 text-center text-gray-500">
          No allocation transfers available at this time.
        </div>
      ) : (
        <div className="grid gap-4">
          {transfers.map(transfer => (
            <TransferCard key={transfer.id} transfer={transfer} />
          ))}
        </div>
      )}
    </div>
  );
}

function TransferCard({ transfer }) {
  const handlePurchaseTransfer = async () => {
    try {
      await purchaseAllocationTransfer(transfer.id);
      // Refresh page or update state
    } catch (error) {
      console.error("Transfer purchase failed:", error);
    }
  };
  
  return (
    <div className="p-4 border rounded-lg">
      <div className="flex justify-between items-start mb-2">
        <div>
          <p className="font-medium">${formatUSDC(transfer.amount)} Allocation</p>
          <p className="text-sm text-gray-600">
            Price: ${formatUSDC(transfer.price)} 
            <span className="text-green-600 ml-1">
              ({((Number(transfer.price) / Number(transfer.amount) - 1) * 100).toFixed(1)}% premium)
            </span>
          </p>
        </div>
        <div className="text-right">
          <p className="text-sm text-gray-500">Expires in</p>
          <p className="font-medium">{formatTimeRemaining(transfer.expiresAt)}</p>
        </div>
      </div>
      
      <div className="flex justify-between items-center">
        <div className="text-xs text-gray-500">
          Seller: {shortenAddress(transfer.sellerAddress)}
        </div>
        <button 
          onClick={handlePurchaseTransfer}
          className="px-4 py-2 bg-green-600 text-white rounded text-sm"
        >
          Purchase Transfer
        </button>
      </div>
    </div>
  );
}

function CreateTransferModal({ allocation, onClose }) {
  const [transferAmount, setTransferAmount] = useState("");
  const [transferPrice, setTransferPrice] = useState("");
  
  const remainingAllocation = BigInt(allocation.MaxAmountUSD) - getCurrentPurchased();
  const transferFee = BigInt(transferPrice || "0") * BigInt(250) / BigInt(10000); // 2.5%
  
  const handleCreateTransfer = async () => {
    try {
      await createAllocationTransfer({
        saleUUID,
        amount: transferAmount,
        price: transferPrice,
        sellerAddress: walletAddress
      });
      onClose();
    } catch (error) {
      console.error("Transfer creation failed:", error);
    }
  };
  
  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
      <div className="bg-white p-6 rounded-lg max-w-md w-full">
        <h3 className="font-medium mb-4">Create Allocation Transfer</h3>
        
        <div className="space-y-4">
          <div>
            <label className="block text-sm font-medium mb-1">
              Transfer Amount (max: ${formatUSDC(remainingAllocation)})
            </label>
            <input
              type="number"
              value={transferAmount}
              onChange={(e) => setTransferAmount(e.target.value)}
              max={Number(formatUSDC(remainingAllocation))}
              className="w-full px-3 py-2 border rounded"
            />
          </div>
          
          <div>
            <label className="block text-sm font-medium mb-1">
              Sale Price (what buyer pays)
            </label>
            <input
              type="number"
              value={transferPrice}
              onChange={(e) => setTransferPrice(e.target.value)}
              className="w-full px-3 py-2 border rounded"
            />
          </div>
          
          {transferPrice && (
            <div className="p-3 bg-gray-50 rounded text-sm">
              <div className="flex justify-between">
                <span>Buyer pays:</span>
                <span>${formatUSDC(transferPrice + "000000")}</span>
              </div>
              <div className="flex justify-between">
                <span>Transfer fee (2.5%):</span>
                <span>-${formatUSDC(transferFee)}</span>
              </div>
              <div className="flex justify-between font-medium border-t pt-1">
                <span>You receive:</span>
                <span>${formatUSDC(BigInt(transferPrice + "000000") - transferFee)}</span>
              </div>
            </div>
          )}
        </div>
        
        <div className="flex gap-3 mt-6">
          <button 
            onClick={onClose}
            className="flex-1 px-4 py-2 border rounded"
          >
            Cancel
          </button>
          <button 
            onClick={handleCreateTransfer}
            className="flex-1 px-4 py-2 bg-blue-600 text-white rounded"
          >
            Create Transfer
          </button>
        </div>
      </div>
    </div>
  );
}

See Also

I