> ## Documentation Index
> Fetch the complete documentation index at: https://luminouslabs-cc5545c6-swen-add-code-runner.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Light Token Standard (Beta)

> Core Concepts of light-mints, light-tokens, and compressed token accounts.

export const RentLifecycleVisualizer = () => {
  const [, setTime] = useState(0);
  const [lamports, setLamports] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const [phase, setPhase] = useState('uninitialized');
  const [hasUserClicked, setHasUserClicked] = useState(false);
  const [showControls, setShowControls] = useState(false);
  const containerRef = useRef(null);
  const [isHighlighted, setIsHighlighted] = useState(false);
  const [activeArrows, setActiveArrows] = useState([]);
  const [activeLines, setActiveLines] = useState([]);
  const [isButtonPressed, setIsButtonPressed] = useState(false);
  const [flyingArrows, setFlyingArrows] = useState([]);
  const [floatingAmounts, setFloatingAmounts] = useState([]);
  const [resetCount, setResetCount] = useState(0);
  const [timelineStarted, setTimelineStarted] = useState(false);
  const LAMPORTS_PER_TICK = 58.2;
  const INITIAL_RENT = 6208;
  const TOPUP_LAMPORTS = 776;
  const TOPUP_THRESHOLD = 776;
  const COLD_THRESHOLD = 388;
  const GREY = {
    r: 161,
    g: 161,
    b: 170
  };
  const RED = {
    r: 227,
    g: 89,
    b: 48
  };
  const BLUE = {
    r: 120,
    g: 140,
    b: 180
  };
  const txLines = [{
    id: 0,
    x1: 5,
    y1: 20,
    x2: 50,
    y2: 50
  }, {
    id: 1,
    x1: 95,
    y1: 15,
    x2: 50,
    y2: 50
  }, {
    id: 2,
    x1: 0,
    y1: 50,
    x2: 50,
    y2: 50
  }, {
    id: 3,
    x1: 100,
    y1: 55,
    x2: 50,
    y2: 50
  }, {
    id: 4,
    x1: 10,
    y1: 85,
    x2: 50,
    y2: 50
  }, {
    id: 5,
    x1: 90,
    y1: 90,
    x2: 50,
    y2: 50
  }, {
    id: 6,
    x1: 50,
    y1: 0,
    x2: 50,
    y2: 50
  }];
  const interpolateColor = (c1, c2, t) => {
    const clamp = v => Math.max(0, Math.min(255, Math.round(v)));
    return {
      r: clamp(c1.r + (c2.r - c1.r) * t),
      g: clamp(c1.g + (c2.g - c1.g) * t),
      b: clamp(c1.b + (c2.b - c1.b) * t)
    };
  };
  const colorToRgba = (c, alpha = 1) => `rgba(${c.r}, ${c.g}, ${c.b}, ${alpha})`;
  const formatLamports = l => {
    if (l <= 0) return '0';
    const rounded = Math.round(l / 500) * 500;
    return `~${rounded.toLocaleString()}`;
  };
  const arrowIdRef = useRef(0);
  const flyingArrowIdRef = useRef(0);
  const floatingAmountIdRef = useRef(0);
  const triggerFlyingArrow = (amount, lineIndex) => {
    const id = flyingArrowIdRef.current++;
    setFlyingArrows(prev => [...prev, id]);
    setTimeout(() => {
      setFlyingArrows(prev => prev.filter(a => a !== id));
    }, 600);
    const amountId = floatingAmountIdRef.current++;
    const line = txLines[lineIndex] || txLines[0];
    setFloatingAmounts(prev => [...prev, {
      id: amountId,
      amount,
      x: line.x1,
      y: line.y1
    }]);
    setTimeout(() => {
      setFloatingAmounts(prev => prev.filter(a => a.id !== amountId));
    }, 800);
  };
  const triggerHighlight = () => {
    setIsHighlighted(true);
    setTimeout(() => setIsHighlighted(false), 500);
    const arrowId = arrowIdRef.current++;
    setActiveArrows(prev => [...prev, arrowId]);
    setTimeout(() => {
      setActiveArrows(prev => prev.filter(id => id !== arrowId));
    }, 500);
  };
  const triggerTransaction = lineIndex => {
    setActiveLines(prev => [...prev, {
      id: lineIndex,
      startTime: Date.now()
    }]);
    setTimeout(() => {
      setActiveLines(prev => prev.filter(l => l.id !== lineIndex));
    }, 500);
  };
  const txLineIndexRef = useRef(0);
  const lastLineIndexRef = useRef(0);
  const getNextLineIndex = () => {
    const index = txLineIndexRef.current;
    txLineIndexRef.current = (txLineIndexRef.current + 1) % txLines.length;
    lastLineIndexRef.current = index;
    return index;
  };
  const getAccountColor = () => {
    if (phase === 'uninitialized') return GREY;
    if (phase === 'cold') return BLUE;
    if (lamports > TOPUP_THRESHOLD) {
      return RED;
    } else if (lamports > COLD_THRESHOLD) {
      const t = 1 - (lamports - COLD_THRESHOLD) / (TOPUP_THRESHOLD - COLD_THRESHOLD);
      return interpolateColor(RED, BLUE, t);
    } else {
      return BLUE;
    }
  };
  const handleTopup = () => {
    triggerTransaction(getNextLineIndex());
    setIsButtonPressed(true);
    setTimeout(() => setIsButtonPressed(false), 200);
    if (phase === 'uninitialized' || phase === 'cold' || lamports === 0) {
      setLamports(INITIAL_RENT);
      setPhase('hot');
      triggerHighlight();
      triggerFlyingArrow(INITIAL_RENT, lastLineIndexRef.current);
      return;
    }
    if (lamports < TOPUP_THRESHOLD) {
      setLamports(l => l + TOPUP_LAMPORTS);
      triggerHighlight();
      triggerFlyingArrow(TOPUP_LAMPORTS, lastLineIndexRef.current);
    }
  };
  const txTimesRef = useRef([1.3, 2, 2.7, 3.3, 4, 4.7, 5.3, 6, 6.7, 7.3, 8, 8.7, 9.3, 10.1, 11.5, 12.8, 16, 16.7, 17.3, 18, 18.7, 19.3, 20, 20.7, 21.3, 22, 22.7, 23.3, 24, 24.7, 25.5, 26.8]);
  const handleReset = () => {
    setTime(0);
    setLamports(0);
    setPhase('uninitialized');
    setIsRunning(true);
    setTimelineStarted(false);
    setActiveLines([]);
    setActiveArrows([]);
    setFlyingArrows([]);
    setResetCount(c => c + 1);
    txLineIndexRef.current = 0;
    arrowIdRef.current = 0;
    flyingArrowIdRef.current = 0;
  };
  const handleDiamondClick = () => {
    if (!hasUserClicked) {
      setHasUserClicked(true);
      setIsRunning(true);
      setShowControls(true);
    }
  };
  useEffect(() => {
    if (!isRunning) return;
    const interval = setInterval(() => {
      setTime(t => {
        const newTime = t + 0.1;
        if (t < 1.0 && newTime >= 1.0) {
          setLamports(INITIAL_RENT);
          setPhase('hot');
          setTimelineStarted(true);
          triggerHighlight();
          triggerTransaction(getNextLineIndex());
        }
        txTimesRef.current.forEach(txTime => {
          if (newTime >= txTime && t < txTime) {
            triggerTransaction(getNextLineIndex());
            if (phase === 'cold') {
              setLamports(INITIAL_RENT);
              setPhase('hot');
              triggerHighlight();
              triggerFlyingArrow(INITIAL_RENT, lastLineIndexRef.current);
            } else if (phase === 'hot') {
              setLamports(currentLamports => {
                if (currentLamports > 0 && currentLamports < TOPUP_THRESHOLD) {
                  triggerHighlight();
                  triggerFlyingArrow(TOPUP_LAMPORTS, lastLineIndexRef.current);
                  return currentLamports + TOPUP_LAMPORTS;
                }
                return currentLamports;
              });
            }
          }
        });
        if ((phase === 'hot' || phase === 'cold') && newTime > 0.1) {
          setLamports(l => {
            const tickAmount = LAMPORTS_PER_TICK;
            const newLamports = Math.max(0, l - tickAmount);
            if (newLamports < COLD_THRESHOLD) {
              setPhase('cold');
            }
            return newLamports;
          });
        }
        if (newTime >= 29) {
          setPhase('uninitialized');
          setLamports(0);
          setHasUserClicked(false);
          setIsRunning(false);
          setShowControls(false);
          setTimelineStarted(false);
          txLineIndexRef.current = 0;
          return 0;
        }
        return newTime;
      });
    }, 100);
    return () => clearInterval(interval);
  }, [isRunning, phase]);
  const accountColor = getAccountColor();
  const generateDiamondDots = () => {
    const dots = [];
    const size = 5;
    const centerSize = 4;
    for (let row = -size; row <= size; row++) {
      const width = size - Math.abs(row);
      for (let col = -width; col <= width; col++) {
        const distFromCenter = Math.max(Math.abs(row), Math.abs(col));
        const fadeProgress = distFromCenter / size;
        const dotSize = Math.max(0.3, centerSize * Math.pow(1 - fadeProgress, 1.5));
        const opacity = Math.pow(1 - fadeProgress, 2.5);
        dots.push({
          x: 50 + col * 6,
          y: 50 + row * 6,
          size: dotSize,
          opacity
        });
      }
    }
    return dots;
  };
  const diamondDots = generateDiamondDots();
  const isLineActive = lineId => activeLines.some(l => l.id === lineId);
  return <div ref={containerRef} className="relative p-6 my-4 overflow-hidden" style={{
    fontFamily: "'Inter', 'IBM Plex Mono'"
  }}>
      {}
      <style>{`
        @keyframes scrollTimeline {
          from { transform: translateX(15rem); }
          to { transform: translateX(-95rem); }
        }
        .timeline-scroll {
          animation: scrollTimeline 29s linear infinite;
          animation-play-state: paused;
          animation-fill-mode: backwards;
        }
        .timeline-scroll-running {
          animation-play-state: running;
        }
        @keyframes bobbleMove {
          0% { offset-distance: 0%; opacity: 0; }
          10% { opacity: 1; }
          90% { opacity: 1; }
          100% { offset-distance: 100%; opacity: 0; }
        }
        .tx-bobble {
          animation: bobbleMove 0.5s ease-out forwards;
        }
        .btn-interactive {
          position: relative;
          overflow: hidden;
          background: rgba(120, 140, 180, 0.06);
          border: 1px solid rgba(120, 140, 180, 0.2);
          border-bottom-color: rgba(0, 0, 0, 0.08);
          box-shadow: 0 1px 2px rgba(0,0,0,0.05);
          color: rgb(0, 0, 0);
        }
        .dark .btn-interactive {
          background: rgba(120, 140, 180, 0.1);
          border: 1px solid rgba(120, 140, 180, 0.25);
          border-bottom-color: rgba(0, 0, 0, 0.2);
          box-shadow: 0 1px 2px rgba(0,0,0,0.1);
          color: rgb(255, 255, 255);
        }
        .btn-interactive:hover {
          background: rgba(120, 140, 180, 0.1);
          box-shadow: 0 2px 4px rgba(0,0,0,0.08);
        }
        .dark .btn-interactive:hover {
          background: rgba(120, 140, 180, 0.15);
          box-shadow: 0 2px 4px rgba(0,0,0,0.15);
        }
        .btn-interactive:active {
          background: rgba(120, 140, 180, 0.12);
          box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
          transform: translateY(0.5px);
        }
        .dark .btn-interactive:active {
          background: rgba(120, 140, 180, 0.18);
          box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);
        }
        @keyframes arrowUp {
          0% { opacity: 0; transform: translateY(calc(-50% + 4px)); }
          20% { opacity: 1; }
          80% { opacity: 1; }
          100% { opacity: 0; transform: translateY(calc(-50% - 8px)); }
        }
        .arrow-up {
          animation: arrowUp 0.5s ease-out forwards;
        }
        @keyframes arrowFlyUp {
          0% { opacity: 1; transform: translateY(calc(-50% + 24px)); }
          80% { opacity: 1; }
          100% { opacity: 0; transform: translateY(calc(-50% - 8px)); }
        }
        .arrow-fly-up {
          animation: arrowFlyUp 0.4s ease-out forwards;
        }
        @keyframes amountFlyUp {
          0% { opacity: 1; transform: translateY(0); }
          70% { opacity: 1; }
          100% { opacity: 0; transform: translateY(-20px); }
        }
        .amount-fly-up {
          animation: amountFlyUp 0.8s ease-out forwards;
        }
      `}</style>

      {}
      <div className="relative flex items-center justify-center" style={{
    height: '11.5rem'
  }}>
        {}
        <svg className="absolute inset-0 w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" style={{
    filter: !hasUserClicked ? 'blur(2px)' : 'none',
    transition: 'filter 0.3s ease'
  }}>
          {txLines.map(line => {
    const active = isLineActive(line.id);
    return <g key={line.id}>
                {}
                <line x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2} stroke={active ? 'rgba(161, 161, 170, 0.5)' : 'rgba(161, 161, 170, 0)'} strokeWidth={active ? 0.8 : 0} style={{
      transition: 'stroke 0.15s, stroke-width 0.15s'
    }} />
                {}
                {active && <circle r="2" fill="rgba(161, 161, 170, 0.8)" style={{
      offsetPath: `path('M ${line.x1} ${line.y1} L ${line.x2} ${line.y2}')`
    }} className="tx-bobble" />}
              </g>;
  })}
          {}
          {floatingAmounts.map(({id, amount, x, y}) => <text key={id} x={x} y={y} className="amount-fly-up" style={{
    fill: 'rgb(34, 197, 94)',
    fontSize: '8px',
    fontWeight: 700,
    fontFamily: 'ui-monospace, monospace',
    textAnchor: 'middle',
    dominantBaseline: 'middle'
  }}>
              +{amount.toLocaleString()}
            </text>)}
        </svg>

        {}
        <div className="absolute inset-0 flex items-center justify-center" style={{
    maskImage: 'linear-gradient(to right, transparent, black 15%, black 35%, transparent 45%, transparent 55%, black 65%, black 85%, transparent)',
    WebkitMaskImage: 'linear-gradient(to right, transparent, black 15%, black 35%, transparent 45%, transparent 55%, black 65%, black 85%, transparent)',
    filter: !hasUserClicked ? 'blur(2px)' : 'none',
    transition: 'filter 0.3s ease'
  }}>
          {}
          <div className="absolute inset-0 flex items-center overflow-hidden">
            <div key={resetCount} className={`flex items-center timeline-scroll ${timelineStarted ? 'timeline-scroll-running' : ''}`} style={{
    gap: '5rem'
  }}>
              {[...Array(34).keys()].map(i => i * 3).concat([...Array(34).keys()].map(i => i * 3)).map((h, i) => <div key={i} className="flex flex-col items-center flex-shrink-0">
                  <span className="font-mono text-zinc-300 dark:text-white/20 mb-2" style={{
    fontSize: '1rem',
    opacity: timelineStarted ? 1 : 0,
    filter: timelineStarted ? 'blur(0)' : 'blur(8px)',
    transition: 'opacity 0.5s ease, filter 0.5s ease'
  }}>
                    {h}h
                  </span>
                  <div className="w-px h-3 bg-zinc-200 dark:bg-white/20" />
                </div>)}
            </div>
          </div>

          {}
          <div className="absolute left-0 right-0 h-px bg-gradient-to-r from-transparent via-zinc-300 dark:via-white/30 to-transparent" />
        </div>

        {}
        <div className="absolute z-10" onClick={handleDiamondClick} style={{
    left: '50%',
    top: '50%',
    transform: 'translate(-50%, -50%)',
    cursor: !hasUserClicked ? 'pointer' : 'default',
    filter: activeLines.length > 0 ? 'drop-shadow(0 0 25px rgba(227, 89, 48, 0.7)) drop-shadow(0 0 10px rgba(255, 150, 50, 0.8))' : 'none',
    transition: 'filter 0.15s ease'
  }}>
          <svg width="138" height="138" viewBox="0 0 100 100">
            {diamondDots.map((dot, i) => <circle key={i} cx={dot.x} cy={dot.y} r={dot.size} fill={colorToRgba(accountColor, dot.opacity * 0.7)} style={{
    transition: 'fill 0.3s ease'
  }} />)}
          </svg>
          {}
          {!hasUserClicked && <div className="text-zinc-400 dark:text-white/40 text-center" style={{
    fontSize: '1.15rem',
    position: 'absolute',
    top: '100%',
    left: '50%',
    transform: 'translateX(-50%)',
    marginTop: '0.5rem',
    whiteSpace: 'nowrap'
  }}>
              Press to see the Rent Config over time!
            </div>}
        </div>
      </div>

      {}
      <div style={{
    opacity: showControls ? 1 : 0,
    filter: showControls ? 'blur(0px)' : 'blur(8px)',
    transition: 'opacity 0.5s ease, filter 0.5s ease',
    pointerEvents: showControls ? 'auto' : 'none'
  }}>
        {}
        <div className="flex justify-center mt-4">
          {}
          <div className="relative flex items-center justify-end mr-2" style={{
    width: '1.5rem',
    height: '2.2rem'
  }}>
            {activeArrows.map(arrowId => <span key={arrowId} className="arrow-up absolute" style={{
    color: 'rgb(34, 197, 94)',
    fontSize: '1.7rem',
    right: 0,
    top: '50%',
    transform: 'translateY(-50%)',
    lineHeight: 1
  }}>
                ↑
              </span>)}
            {flyingArrows.map(id => <span key={id} className="arrow-fly-up absolute" style={{
    color: 'rgb(34, 197, 94)',
    fontSize: '1.7rem',
    right: 0,
    top: '50%',
    lineHeight: 1
  }}>
                ↑
              </span>)}
          </div>
          <div className="text-center">
            <div style={{
    height: '2.2rem',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  }}>
              <span className="font-mono text-zinc-700 dark:text-white/80 transition-all duration-150" style={{
    fontSize: isHighlighted ? '1.9rem' : '1.7rem',
    fontWeight: isHighlighted ? 700 : 500,
    transformOrigin: 'center',
    transform: isHighlighted ? 'scale(1.05)' : 'scale(1)',
    fontVariantNumeric: 'tabular-nums',
    minWidth: '6.5rem',
    textAlign: 'right'
  }}>
                {formatLamports(lamports)}
              </span>
              <span className="text-zinc-400 dark:text-white/40 transition-all duration-150 ml-1" style={{
    fontSize: isHighlighted ? '1.45rem' : '1.15rem',
    fontWeight: isHighlighted ? 800 : 400
  }}>lamports</span>
            </div>
            <div className="text-zinc-500 dark:text-white/50 uppercase tracking-wide" style={{
    fontSize: '0.9rem'
  }}>
              Rent Balance
            </div>
          </div>
        </div>

        {}
        <div className="flex justify-center gap-4 mt-3">
          <button onClick={handleReset} className="font-medium rounded-lg border backdrop-blur-sm transition-all btn-interactive" style={{
    padding: '0.5rem 1rem',
    fontSize: '0.85rem'
  }}>
            Back to Start
          </button>
          <button onClick={handleTopup} className={`rounded-lg border-none backdrop-blur-sm transition-all ${isButtonPressed ? 'font-bold' : 'font-medium'}`} style={{
    padding: '0.5rem 1rem',
    fontSize: isButtonPressed ? '0.95rem' : '0.85rem',
    transform: isButtonPressed ? 'scale(1.15)' : 'scale(1)',
    background: '#0066ff',
    color: '#fff'
  }}>
            Send Tx
          </button>
        </div>
      </div>
    </div>;
};

export const CompressibleRentCalculator = () => {
  const [hours, setHours] = useState(24);
  const [lamportsPerWrite, setLamportsPerWrite] = useState(776);
  const [showCustomHours, setShowCustomHours] = useState(false);
  const [showCustomLamports, setShowCustomLamports] = useState(false);
  const [showFormula, setShowFormula] = useState(false);
  const DATA_LEN = 260;
  const BASE_RENT = 128;
  const LAMPORTS_PER_BYTE_PER_EPOCH = 1;
  const MINUTES_PER_EPOCH = 90;
  const COMPRESSION_INCENTIVE = 11000;
  const LAMPORTS_PER_SOL = 1_000_000_000;
  const HOURS_MAX = 36;
  const LAMPORTS_MAX = 6400;
  const numEpochs = Math.ceil(hours * 60 / MINUTES_PER_EPOCH);
  const rentPerEpoch = BASE_RENT + DATA_LEN * LAMPORTS_PER_BYTE_PER_EPOCH;
  const totalPrepaidRent = rentPerEpoch * numEpochs;
  const totalCreationCost = totalPrepaidRent + COMPRESSION_INCENTIVE;
  const handleHoursChange = value => {
    const num = Math.max(3, Math.min(168, Number.parseInt(value) || 3));
    setHours(num);
  };
  const handleLamportsChange = value => {
    const num = Math.max(0, Math.min(100000, Number.parseInt(value) || 0));
    setLamportsPerWrite(num);
  };
  const hoursPresets = [24];
  const lamportsPresets = [776];
  const SliderMarkers = ({max, step}) => {
    const marks = [];
    for (let i = step; i < max; i += step) {
      const percent = i / max * 100;
      marks.push(<div key={i} className="absolute top-1/2 -translate-y-1/2 w-px h-2 bg-zinc-300 dark:bg-white/30" style={{
        left: `${percent}%`
      }} />);
    }
    return <>{marks}</>;
  };
  return <div className="p-5 rounded-3xl not-prose mt-4 dark:bg-white/5 backdrop-blur-xl border border-black/[0.04] dark:border-white/10 shadow-lg" style={{
    fontFamily: 'Inter, sans-serif'
  }}>
      <div className="space-y-5">
        {}
        <div className="space-y-2 px-3">
          <div className="flex justify-between items-center">
            <span className="text-sm text-zinc-700 dark:text-white/80">Prepaid Epochs in Hours</span>
            <div className="flex items-center gap-1.5">
              {hoursPresets.map(h => <button key={h} onClick={() => {
    setHours(h);
    setShowCustomHours(false);
  }} className={`px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all ${hours === h && !showCustomHours ? 'bg-blue-500/20 border-blue-500/50 text-blue-600 dark:text-blue-400' : 'bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]'}`}>
                  {h === 24 ? 'Default' : `${h}h`}
                </button>)}
              {showCustomHours ? <input type="number" min="3" max="168" value={hours} onChange={e => handleHoursChange(e.target.value)} className="w-16 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" autoFocus /> : <button onClick={() => setShowCustomHours(true)} className="px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]">
                  Custom
                </button>}
            </div>
          </div>
          <div className="flex items-center">
            <span className="w-1/3 text-xs text-zinc-500 dark:text-white/50 whitespace-nowrap">
              ≈ {(hours * 60 / MINUTES_PER_EPOCH).toFixed(1)} epochs / {hours.toFixed(1)}h
            </span>
            <div className="w-2/3 relative">
              <SliderMarkers max={HOURS_MAX} step={2} />
              <input type="range" min="3" max={HOURS_MAX} value={Math.min(hours, HOURS_MAX)} onChange={e => {
    setHours(Number.parseInt(e.target.value));
    setShowCustomHours(false);
  }} className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" />
            </div>
          </div>
        </div>

        {}
        <div className="space-y-2 px-3">
          <div className="flex justify-between items-center">
            <span className="text-sm text-zinc-700 dark:text-white/80">Lamports per Write</span>
            <div className="flex items-center gap-1.5">
              {lamportsPresets.map(l => <button key={l} onClick={() => {
    setLamportsPerWrite(l);
    setShowCustomLamports(false);
  }} className={`px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all ${lamportsPerWrite === l && !showCustomLamports ? 'bg-blue-500/20 border-blue-500/50 text-blue-600 dark:text-blue-400' : 'bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]'}`}>
                  {l === 776 ? 'Default' : l.toLocaleString()}
                </button>)}
              {showCustomLamports ? <input type="number" min="0" max="100000" value={lamportsPerWrite} onChange={e => handleLamportsChange(e.target.value)} className="w-20 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" autoFocus /> : <button onClick={() => setShowCustomLamports(true)} className="px-2.5 py-1 text-xs font-medium rounded-lg border backdrop-blur-sm transition-all bg-black/[0.015] dark:bg-white/5 border-black/[0.04] dark:border-white/20 text-zinc-600 dark:text-white/70 hover:bg-black/[0.03]">
                  Custom
                </button>}
            </div>
          </div>
          <div className="flex items-center">
            <span className="w-1/3 text-xs text-zinc-500 dark:text-white/50 whitespace-nowrap">
              ≈ {(lamportsPerWrite / rentPerEpoch).toFixed(1)} epochs / {(lamportsPerWrite / rentPerEpoch * MINUTES_PER_EPOCH / 60).toFixed(1)}h
            </span>
            <div className="w-2/3 relative">
              <SliderMarkers max={LAMPORTS_MAX} step={800} />
              <input type="range" min="0" max={LAMPORTS_MAX} step="100" value={Math.min(lamportsPerWrite, LAMPORTS_MAX)} onChange={e => {
    setLamportsPerWrite(Number.parseInt(e.target.value));
    setShowCustomLamports(false);
  }} className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" />
            </div>
          </div>
        </div>

        {}
        <div className="grid grid-cols-2 gap-3">
          <div className="p-4 bg-black/[0.015] dark:bg-white/5 backdrop-blur-md rounded-2xl text-center border border-black/[0.04] dark:border-white/10 shadow-sm">
            <div className="text-xs text-zinc-500 dark:text-white/50 mb-1 uppercase tracking-wide">Total Creation Cost</div>
            <div className="text-xl font-mono font-semibold text-zinc-900 dark:text-white">
              {totalCreationCost.toLocaleString()}
            </div>
            <div className="text-xs text-zinc-400 dark:text-white/40">lamports</div>
            <div className="text-xs text-zinc-500 dark:text-white/50 mt-1">≈ {(totalCreationCost / LAMPORTS_PER_SOL).toFixed(6)} SOL</div>
          </div>

          <div className="p-4 bg-black/[0.015] dark:bg-white/5 backdrop-blur-md rounded-2xl text-center border border-black/[0.04] dark:border-white/10 shadow-sm">
            <div className="text-xs text-zinc-500 dark:text-white/50 mb-1 uppercase tracking-wide">Top-up Amount</div>
            <div className="text-xl font-mono font-semibold text-zinc-900 dark:text-white">
              {lamportsPerWrite.toLocaleString()}
            </div>
            <div className="text-xs text-zinc-400 dark:text-white/40">lamports</div>
            <div className="text-xs text-zinc-500 dark:text-white/50 mt-1">≈ {(lamportsPerWrite / LAMPORTS_PER_SOL).toFixed(6)} SOL</div>
          </div>
        </div>

        {}
        <div className="pt-3 border-t border-black/[0.04] dark:border-white/10">
          <button onClick={() => setShowFormula(!showFormula)} className="flex items-center gap-2 text-xs text-zinc-500 dark:text-white/50 hover:text-zinc-700 dark:hover:text-white/70 transition-colors">
            <span className={`transition-transform ${showFormula ? 'rotate-90' : ''}`}>▶</span>
            Show formula
          </button>
          {showFormula && <div className="text-xs font-mono text-zinc-500 dark:text-white/40 mt-3">
              <div className="text-zinc-600 dark:text-white/60 mb-2">Total cost for {DATA_LEN}-byte light-token account:</div>
              total_creation_cost = prepaid_rent + compression_incentive<br /><br />
              rent_per_epoch = base_rent + (data_len × lamports_per_byte_per_epoch)<br />
              rent_per_epoch = {BASE_RENT} + ({DATA_LEN} × {LAMPORTS_PER_BYTE_PER_EPOCH}) = {rentPerEpoch} lamports<br />
              compression_incentive = {COMPRESSION_INCENTIVE.toLocaleString()} lamports
            </div>}
        </div>
      </div>
    </div>;
};

The [Light Token Program](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token)
is a high performance token program that reduces the cost of account creations by 200x,
while being more CU efficient than SPL on hot paths.

<table>
  <thead>
    <tr>
      <th style={{ width: "15%", textAlign: "left" }} />

      <th style={{ width: "22%", textAlign: "left" }}>Account Type</th>
      <th style={{ width: "63%", textAlign: "left" }}>Key Features</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td style={{ textAlign: "left" }}>
        **[Mint Accounts](#light-mint-accounts)**
      </td>

      <td style={{ textAlign: "left" }}>Compressed account</td>

      <td style={{ textAlign: "left" }}>
        <ul>
          <li>**Rent-free mint accounts** (similar to SPL mints)</li>
          <li>Custom [Token Metadata](#token-metadata) **extension**</li>
        </ul>
      </td>
    </tr>

    <tr>
      <td style={{ textAlign: "left" }}>
        **[Token Accounts](#light-token-account)**
      </td>

      <td style={{ textAlign: "left" }}>Solana account</td>

      <td style={{ textAlign: "left" }}>
        <ul>
          <li>
            **Stores token balances** of mints (SPL, Token-2022, or Light)
          </li>

          <li>**Sponsored rent** exemption via custom rent-config</li>
        </ul>
      </td>
    </tr>
  </tbody>
</table>

# Mint Accounts

<Note>
  * **Light-mints are compressed accounts** and **cannot be decompressed**. \*
    SPL mints can not be compressed to light-mints.
</Note>

Light-mints **uniquely represent a token on Solana and store its global metadata**, similar to SPL mint accounts with few core differences:

1. Light-mint accounts are **rent-free**.
2. Tokens created from light-mints are **light-tokens**.
3. Token metadata (name, symbol, URI) is stored as an extension in the struct.

| Creation Cost    | SPL                  | Light               |
| :--------------- | :------------------- | :------------------ |
| **Mint Account** | \~1,500,000 lamports | **15,000** lamports |

<Tabs>
  <Tab title="Diagram">
    <Frame>
      <img src="https://mintcdn.com/luminouslabs-cc5545c6-swen-add-code-runner/kjqHHw14Q8G9AGL6/images/mint-account-layout.png?fit=max&auto=format&n=kjqHHw14Q8G9AGL6&q=85&s=533ef902342149d462f1c187f7bfbc4b" alt="Diagram showing light-mint compressed account structure with three components: Hash (identifier for light-mint in purple box), Account (struct containing BaseMint with SPL-compatible fields, light-mint Data for program state, and optional Extensions for Token Metadata), and BasemintData (containing Supply, Decimals, Mint Authority, and Freeze Authority fields) with Token Metadata extension" width="6312" height="2436" data-path="images/mint-account-layout.png" />
    </Frame>
  </Tab>

  <Tab title="Source Code">
    ```rust theme={null}
    pub struct CompressedMint {
        // SPL mint layout
        pub base: BaseMint,
        // light-mint state used by the Compressed Token Program
        pub metadata: CompressedMintMetadata
        // Field for Token Metadata extension
        pub extensions: Option<Vec<ExtensionStruct>>,
    }
    ```
  </Tab>
</Tabs>

<Info>
  Find the [source code of light-mints
  here](https://github.com/Lightprotocol/light-protocol/blob/0c4e2417b2df2d564721b89e18d1aad3665120e7/program-libs/ctoken-interface/src/state/mint/compressed_mint.rs#L18).
</Info>

The `metadata` field is used by the Compressed Token Program to store the internal state of a light-mint.

The `BaseMint` field replicates the field layout and serialization format of [SPL Mint accounts](https://solana.com/docs/tokens#mint-account). The struct is serialized with Borsh to match the on-chain format of SPL tokens and mints.

Here is how light-mints and SPL mints compare:

<Tabs>
  <Tab title="Basemint vs SPL mint">
    <table>
      <thead>
        <tr>
          <th style={{width: '10%', textAlign: 'left'}}>Field</th>
          <th style={{width: '45%', textAlign: 'center'}}>Light-Mint</th>
          <th style={{width: '30%', textAlign: 'center'}}>SPL Mint</th>
        </tr>
      </thead>

      <tbody>
        <tr>
          <td>mint\_authority</td>
          <td style={{textAlign: 'center'}}>✓</td>
          <td style={{textAlign: 'center'}}>✓</td>
        </tr>

        <tr>
          <td>supply</td>
          <td style={{textAlign: 'center'}}>✓</td>
          <td style={{textAlign: 'center'}}>✓</td>
        </tr>

        <tr>
          <td>decimals</td>
          <td style={{textAlign: 'center'}}>✓</td>
          <td style={{textAlign: 'center'}}>✓</td>
        </tr>

        <tr>
          <td>is\_initialized</td>
          <td style={{textAlign: 'center'}}>✓</td>
          <td style={{textAlign: 'center'}}>✓</td>
        </tr>

        <tr>
          <td>freeze\_authority</td>
          <td style={{textAlign: 'center'}}>✓</td>
          <td style={{textAlign: 'center'}}>✓</td>
        </tr>

        <tr>
          <td>Light-Mint Data</td>
          <td style={{textAlign: 'center'}}>✓</td>
          <td style={{textAlign: 'center'}}>-</td>
        </tr>

        <tr>
          <td>Extensions</td>
          <td style={{textAlign: 'center'}}>✓</td>
          <td style={{textAlign: 'center'}}>via Token-2022</td>
        </tr>
      </tbody>
    </table>
  </Tab>

  <Tab title="BaseMint Struct">
    ```rust theme={null}
    pub struct BaseMint {
        /// Optional authority used to mint new tokens. The mint authority may only
        /// be provided during mint creation. If no mint authority is present
        /// then the mint has a fixed supply and no further tokens may be
        /// minted.
        pub mint_authority: Option<Pubkey>,
        /// Total supply of tokens.
        pub supply: u64,
        /// Number of base 10 digits to the right of the decimal place.
        pub decimals: u8,
        /// Is initialized - for SPL compatibility
        pub is_initialized: bool,
        /// Optional authority to freeze token accounts.
        pub freeze_authority: Option<Pubkey>,
    }
    ```
  </Tab>
</Tabs>

# Token Account

<Note>
  **Light token accounts are Solana accounts**, not compressed accounts.
</Note>

A light-token account holds token balances like SPL Token accounts:

* A wallet needs a light-token account for each light-mint, SPL mint, or Token 2022 mint it wants to hold, with the wallet address set as the light-token account owner.
* Each wallet can own multiple light-token accounts for the same light-mint.
* A light-token account can only have one owner and hold units of one light-mint.

| Creation Cost     | SPL                  | Light                 |
| :---------------- | :------------------- | :-------------------- |
| **Token Account** | \~2,000,000 lamports | \~**11,000** lamports |

Additionally Light Token is more performant then SPL on hot paths:

|                                   | light-token CU | SPL-token CU |
| --------------------------------- | -------------: | -----------: |
| **ATA Creation**                  |          4,348 |       14,194 |
| **Transfer** (base path)          |            312 |        4,645 |
| **Transfer** (rent-free hot path) |          1,885 |        4,645 |

<Tabs>
  <Tab title="Diagram">
    <Frame>
      <img src="https://mintcdn.com/luminouslabs-cc5545c6-swen-add-code-runner/CPEpqHEIvSqmEY-X/images/token-account-layout.png?fit=max&auto=format&n=CPEpqHEIvSqmEY-X&q=85&s=713943ea845d8f708fa69b1a82e8d0a2" alt="Diagram showing light-token Solana account structure with three components: Address (identifier for light-token account in purple box), Account (struct containing Data bytes, Executable flag, Lamports balance, and Owner set to Compressed Token Program), and AccountData (containing Mint, Owner, Amount, and Extensions fields)" width="6304" height="2527" data-path="images/token-account-layout.png" />
    </Frame>
  </Tab>

  <Tab title="Source Code">
    ```rust theme={null}
    /// Ctoken account structure (with extensions).
    pub struct CToken {
        pub mint: Pubkey,
        pub owner: Pubkey,
        pub amount: u64,
        pub delegate: Option<Pubkey>, // instruction not implemented yet
        pub state: u8,
        pub is_native: Option<u64>,
        pub delegated_amount: u64, // instruction not implemented yet
        pub close_authority: Option<Pubkey>,
        pub extensions: Option<Vec<ExtensionStruct>>, // Optional extensions e.g. compressible
    }
    ```
  </Tab>
</Tabs>

<Info>
  Find the [source code of light-tokens
  here](https://github.com/Lightprotocol/light-protocol/blob/0c4e2417b2df2d564721b89e18d1aad3665120e7/program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs#L31).
</Info>

Light token accounts replicate the field layout and serialization format of [SPL Token accounts](https://solana.com/docs/tokens#token-account). The struct is serialized with Borsh to match the on-chain format of SPL tokens.

Here is how light-tokens and SPL tokens compare:

<table>
  <thead>
    <tr>
      <th style={{ width: "10%", textAlign: "left" }}>Field</th>
      <th style={{ width: "45%", textAlign: "center" }}>Light Token</th>
      <th style={{ width: "30%", textAlign: "center" }}>SPL Token Account</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>mint</td>
      <td style={{ textAlign: "center" }}>✓</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>owner</td>
      <td style={{ textAlign: "center" }}>✓</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>amount</td>
      <td style={{ textAlign: "center" }}>✓</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>delegate</td>
      <td style={{ textAlign: "center" }}>unimplemented</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>state</td>
      <td style={{ textAlign: "center" }}>✓</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>is\_native</td>
      <td style={{ textAlign: "center" }}>✓</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>delegated\_amount</td>
      <td style={{ textAlign: "center" }}>unimplemented</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>close\_authority</td>
      <td style={{ textAlign: "center" }}>✓</td>
      <td style={{ textAlign: "center" }}>✓</td>
    </tr>

    <tr>
      <td>extensions</td>
      <td style={{ textAlign: "center" }}>✓</td>
      <td style={{ textAlign: "center" }}>via Token-2022</td>
    </tr>
  </tbody>
</table>

### Rent Config for Light Tokens

1. The rent-exemption for light-token account creation is sponsored by Light Protocol.
2. Transaction payer's pay rent <Tooltip tip="1 rent-epoch ≈ 1.5h ≈ 13,500 slots at 400ms per slot">per rent-epoch (388 lamports for 1.5h)</Tooltip> <br />
   to keep accounts "active".
3. "Inactive" accounts, where rent is below one epoch, are compressed <br />and the rent-exemption can be claimed by the rent sponsor.
4. Transfers to inactive accounts <Tooltip tip="Compressed state is stored in leaves of Merkle trees (≃ disk). light-token accounts are Solana accounts that store state on chain (≃ RAM). Load means to load state from 'disk' to 'RAM', i.e. decompress accounts.">"load" it with the same state </Tooltip> (decompress).

This way rent is automatically paid when accounts are used:

<table>
  <thead>
    <tr>
      <th style={{textAlign: 'left'}}>Event</th>
      <th style={{textAlign: 'left'}}>Total Cost</th>
      <th style={{textAlign: 'left'}}>Payer</th>
      <th style={{textAlign: 'left'}}>Time of Rent funded</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><strong>Account Creation</strong></td>
      <td><strong><Tooltip tip="6,208 lamports (24h rent) + 11,000 lamports (compression cost & protocol incentive)">\~17,000 lamports</Tooltip></strong></td>
      <td>Transaction payer</td>
      <td>Funds 24h rent</td>
    </tr>

    <tr>
      <td><strong>Automatic Top ups</strong><br />(when rent \< 3h)</td>
      <td><strong>776 lamports</strong></td>
      <td>Transaction payer</td>
      <td>Funds 3h rent</td>
    </tr>

    <tr>
      <td><strong>Load Account</strong><br />(when inactive)</td>
      <td><strong><Tooltip tip="6,208 lamports (24h rent) + 11,000 lamports (compression cost & protocol incentive)">\~17,000 lamports</Tooltip></strong></td>
      <td>Transaction payer</td>
      <td>Funds 24h rent</td>
    </tr>
  </tbody>
</table>

<RentLifecycleVisualizer />

We recommend to use default values, but you can customize prepaid rent and top ups:

<CompressibleRentCalculator />

# Associated Light Token Account

**Associated light-token** accounts (light-ATAs) follow the same pattern as [associated token accounts](https://docs.solana.com/developing/programming-model/associated-token-account) (ATA):

* Each wallet needs its own light-token account to hold tokens from the same light-mint.
* The address for light-ATAs is deterministically derived with the owner's address, light token program ID, and mint address.

```rust theme={null}
let seeds = [
    owner.as_ref(),          // Wallet address (32 bytes)
    program_id.as_ref(),     // Compressed Token Program ID (32 bytes)
    mint.as_ref(),           // light-mint address (32 bytes)
    bump.as_ref(),           // Bump seed (1 byte)
];
```

<Info>
  Find the [source code to associated light-token accounts
  here](https://github.com/Lightprotocol/light-protocol/blob/main/programs/compressed-token/program/src/create_associated_token_account.rs).
</Info>

# Compressed Token Account

Under the hood, compressed token accounts store token balance, owner, and other information of inactive light-tokens.

1. Light token accounts are automatically compressed/decompressed when active/inactive.
2. Any light-token or SPL token can be compressed/decompressed at will.

<Note>
  You can still use compressed tokens for token distribution. Learn more about
  this here.
</Note>

<Tabs>
  <Tab title="Diagram">
    <Frame>
      <img src="https://mintcdn.com/luminouslabs-cc5545c6-swen-add-code-runner/n0RdKn4MEsiWR00J/images/compressed-token-explainer.png?fit=max&auto=format&n=n0RdKn4MEsiWR00J&q=85&s=67fccfd12c2f99874f20ed6465653852" alt="Diagram showing compressed token account structure with three components: Hash (identifier for compressed token account in purple box), Account (struct containing Data bytes, Executable flag, Lamports balance, and Address set to None), and AccountData (containing Mint, Owner, Amount, and Extensions fields marked as unimplemented)" width="6296" height="2558" data-path="images/compressed-token-explainer.png" />
    </Frame>
  </Tab>

  <Tab title="Source Code">
    ```rust theme={null}
    pub struct TokenData {
        pub mint: Pubkey,
        pub owner: Pubkey,
        pub amount: u64,
        pub delegate: Option<Pubkey>,
        pub state: u8,
        /// Placeholder for TokenExtension tlv data (unimplemented)
        pub tlv: Option<Vec<u8>>,
    }
    ```
  </Tab>
</Tabs>

# Next Steps

<Card title="Create Light Token Accounts" icon="chevron-right" color="#0066ff" href="/light-token/cookbook/create-token-account" horizontal />
