> ## 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.

# Create Associated Light Token Accounts

> Client and program guide to create associated light-token accounts. Includes step-by-step implementation and full code examples.

export const CodeCompare = ({firstCode = "", secondCode = "", firstLabel = "Light Token", secondLabel = "SPL"}) => {
  const [sliderPercent, setSliderPercent] = useState(0);
  const [isDragging, setIsDragging] = useState(false);
  const [isAnimating, setIsAnimating] = useState(false);
  const containerRef = useRef(null);
  const animationRef = useRef(null);
  const isLightMode = sliderPercent > 50;
  const highlightCode = code => {
    let escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    const pattern = /(\/\/.*$)|(["'`])(?:(?!\2)[^\\]|\\.)*?\2|\b(const|let|var|await|async|import|from|export|return|if|else|function|class|new|throw|try|catch)\b|\.([a-zA-Z_][a-zA-Z0-9_]*)\b|\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/gm;
    return escaped.replace(pattern, (match, comment, stringQuote, keyword, property, func) => {
      if (comment) return `<span style="color:#6b7280;font-style:italic">${match}</span>`;
      if (stringQuote) return `<span style="color:#059669">${match}</span>`;
      if (keyword) return `<span style="color:#db2777">${match}</span>`;
      if (property) return `.<span style="color:#0891b2">${property}</span>`;
      if (func) return `<span style="color:#2563eb">${match}</span>`;
      return match;
    });
  };
  const animateTo = target => {
    if (animationRef.current) cancelAnimationFrame(animationRef.current);
    setIsAnimating(true);
    const start = sliderPercent;
    const startTime = performance.now();
    const duration = 400;
    const animate = currentTime => {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);
      const eased = 1 - Math.pow(1 - progress, 3);
      const current = start + (target - start) * eased;
      setSliderPercent(current);
      if (progress < 1) {
        animationRef.current = requestAnimationFrame(animate);
      } else {
        setSliderPercent(target);
        setIsAnimating(false);
        animationRef.current = null;
      }
    };
    animationRef.current = requestAnimationFrame(animate);
  };
  const handleToggle = () => {
    animateTo(isLightMode ? 0 : 100);
  };
  const handleMouseDown = e => {
    if (isAnimating) {
      cancelAnimationFrame(animationRef.current);
      setIsAnimating(false);
    }
    e.preventDefault();
    setIsDragging(true);
  };
  const handleMouseUp = () => {
    setIsDragging(false);
  };
  const handleMouseMove = e => {
    if (!isDragging || !containerRef.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const percent = Math.max(0, Math.min(100, x / rect.width * 100));
    setSliderPercent(percent);
  };
  const handleTouchMove = e => {
    if (!containerRef.current) return;
    if (isAnimating) {
      cancelAnimationFrame(animationRef.current);
      setIsAnimating(false);
    }
    const rect = containerRef.current.getBoundingClientRect();
    const x = e.touches[0].clientX - rect.left;
    const percent = Math.max(0, Math.min(100, x / rect.width * 100));
    setSliderPercent(percent);
  };
  const handleKeyDown = e => {
    if (e.key === "ArrowLeft") {
      setSliderPercent(p => Math.max(0, p - 5));
    } else if (e.key === "ArrowRight") {
      setSliderPercent(p => Math.min(100, p + 5));
    }
  };
  useEffect(() => {
    if (isDragging) {
      document.addEventListener("mousemove", handleMouseMove);
      document.addEventListener("mouseup", handleMouseUp);
      return () => {
        document.removeEventListener("mousemove", handleMouseMove);
        document.removeEventListener("mouseup", handleMouseUp);
      };
    }
  }, [isDragging]);
  useEffect(() => {
    return () => {
      if (animationRef.current) cancelAnimationFrame(animationRef.current);
    };
  }, []);
  return <>
      <div className="rounded-3xl not-prose mt-4 backdrop-blur-xl border overflow-hidden" style={{
    fontFamily: 'Inter, sans-serif',
    borderColor: '#d4d4d8'
  }}>
        {}
        <div className="flex items-center justify-between px-4 py-3 border-b" style={{
    background: 'linear-gradient(to bottom, #f8f9fa, #f1f3f4)',
    borderColor: '#e4e4e7'
  }}>
          <span className="text-sm font-medium" style={{
    color: '#52525b'
  }}>
            {isLightMode ? secondLabel : firstLabel}
          </span>

          {}
          <div onClick={handleToggle} style={{
    position: 'relative',
    width: '56px',
    height: '28px',
    background: '#e0e0e0',
    borderRadius: '14px',
    boxShadow: 'inset -2px -2px 4px #ffffff, inset 2px 2px 4px #b0b0b0',
    cursor: 'pointer',
    transition: 'background 0.3s ease, box-shadow 0.3s ease'
  }}>
            {}
            <div style={{
    position: 'absolute',
    width: '24px',
    height: '24px',
    background: 'linear-gradient(145deg, #f5f5f5, #e0e0e0)',
    borderRadius: '12px',
    top: '2px',
    left: isLightMode ? '30px' : '2px',
    boxShadow: '-2px -2px 4px #ffffff, 2px 2px 4px #b0b0b0',
    transition: 'all 0.3s ease-in-out',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  }}>
              {}
              <div style={{
    width: '6px',
    height: '6px',
    background: isLightMode ? '#0066ff' : '#999',
    borderRadius: '50%',
    boxShadow: isLightMode ? '0 0 8px 2px #0066ff' : '0 0 4px 1px rgba(0, 0, 0, 0.1)',
    transition: 'all 0.3s ease-in-out'
  }} />
            </div>
          </div>
        </div>

        {}
        <div ref={containerRef} className="p-0" style={{
    cursor: isDragging ? "grabbing" : "default"
  }} onTouchMove={handleTouchMove} tabIndex={0} onKeyDown={handleKeyDown} role="slider" aria-valuenow={sliderPercent} aria-valuemin={0} aria-valuemax={100} aria-label="Code comparison slider">
          <div className="relative" style={{
    minHeight: "140px",
    overflowX: "auto"
  }}>
            <div style={{
    display: "grid"
  }}>
              {}
              <pre className="m-0 p-4 text-zinc-700 dark:text-white/80 bg-transparent" style={{
    gridArea: "1/1",
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
    fontSize: "13px",
    lineHeight: "1.6",
    whiteSpace: "pre",
    zIndex: 1
  }} dangerouslySetInnerHTML={{
    __html: highlightCode(firstCode)
  }} />

              {}
              <pre className="m-0 p-4 text-zinc-700 dark:text-white/80 bg-white dark:bg-zinc-900" style={{
    gridArea: "1/1",
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
    fontSize: "13px",
    lineHeight: "1.6",
    whiteSpace: "pre",
    zIndex: 2,
    clipPath: `inset(0 ${100 - sliderPercent}% 0 0)`
  }} dangerouslySetInnerHTML={{
    __html: highlightCode(secondCode)
  }} />
            </div>

            {}
            <div className="absolute top-0 bottom-0 flex items-center justify-center pointer-events-none" style={{
    left: `${sliderPercent}%`,
    transform: "translateX(-50%)",
    zIndex: 30
  }}>
              <div className="absolute top-0 bottom-0 w-px bg-zinc-400 dark:bg-white/30" />

              <div className="absolute top-0 bottom-0" style={{
    right: "50%",
    width: "80px",
    background: "linear-gradient(to left, rgba(0, 102, 255, 0.15) 0%, transparent 100%)"
  }} />

              {}
              <div onMouseDown={handleMouseDown} className="pointer-events-auto cursor-grab flex items-center justify-center gap-px transition-transform" style={{
    width: "20px",
    height: "32px",
    borderRadius: "4px",
    background: "#f8fafc",
    border: "1px solid #d1d5db",
    boxShadow: "0 1px 2px rgba(0,0,0,0.05)",
    transform: isDragging ? "scale(1.08)" : "scale(1)"
  }}>
                <div className="flex flex-col gap-0.5">
                  {[0, 1, 2].map(i => <div key={i} style={{
    width: '3px',
    height: '3px',
    borderRadius: '50%',
    background: '#0066ff'
  }} />)}
                </div>
                <div className="flex flex-col gap-0.5">
                  {[0, 1, 2].map(i => <div key={i} style={{
    width: '3px',
    height: '3px',
    borderRadius: '50%',
    background: '#0066ff'
  }} />)}
                </div>
              </div>
            </div>
          </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>;
};

***

1. Associated light-token accounts are Solana accounts that hold token balances of light, SPL, or Token 2022 mints.
2. The address for light-ATAs is deterministically derived with the owner's address, compressed token program ID, and mint address.
3. Associated light-token accounts implement a default rent config:
   1. At account creation, you pay \~17,208 lamports <Tooltip tip="24 h = 16 epochs, where 1 rent-epoch ≈ 1.5h ≈ 13,500 slots at 400ms per slot">for 24h of rent</Tooltip> <br />and <Tooltip tip="Covers transaction cost to compress accounts (10,000) and protocol incentive (1,000). Transaction cost might vary.">compression incentive</Tooltip> (the rent-exemption is sponsored by the protocol)
   2. Transfers keep the account funded <Tooltip tip="2 epochs = 3h">with rent for 3h</Tooltip> via top-ups. The transaction payer tops up 776 lamports when the account's rent is below 3h.

<Accordion title="Light Rent Config Explained">
  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>
</Accordion>

## Get Started

<Tabs>
  <Tab title="TypeScript Client">
    The `createAtaInterface` function creates an associated light-token account in a single call.

    Compare to SPL:

    <CodeCompare
      firstCode={`// SPL create ATA
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";

const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
owner
);`}
      secondCode={`// light-token create ATA
import { createAtaInterface } from "@lightprotocol/compressed-token";

const ata = await createAtaInterface(
rpc,
payer,
mint,
owner
);`}
      firstLabel="SPL"
      secondLabel="light-token"
    />

    <Info>
      Find the source code
      [here](https://github.com/Lightprotocol/light-protocol/blob/0c4e2417b2df2d564721b89e18d1aad3665120e7/js/compressed-token/src/v3/actions/create-ata-interface.ts).
    </Info>

    <Steps>
      <Step>
        ### Create Associated Token Account

        <Accordion title="Installations">
          <Tabs>
            <Tab title="npm">
              Install packages in your working directory:

              ```bash theme={null}
              npm install @lightprotocol/stateless.js@alpha \
                          @lightprotocol/compressed-token@alpha
              ```

              Install the CLI globally:

              ```bash theme={null}
              npm install -g @lightprotocol/zk-compression-cli@alpha
              ```
            </Tab>

            <Tab title="yarn">
              Install packages in your working directory:

              ```bash theme={null}
              yarn add @lightprotocol/stateless.js@alpha \
                       @lightprotocol/compressed-token@alpha
              ```

              Install the CLI globally:

              ```bash theme={null}
              yarn global add @lightprotocol/zk-compression-cli@alpha
              ```
            </Tab>

            <Tab title="pnpm">
              Install packages in your working directory:

              ```bash theme={null}
              pnpm add @lightprotocol/stateless.js@alpha \
                       @lightprotocol/compressed-token@alpha
              ```

              Install the CLI globally:

              ```bash theme={null}
              pnpm add -g @lightprotocol/zk-compression-cli@alpha
              ```
            </Tab>
          </Tabs>
        </Accordion>

        ```bash theme={null}
        # Start local test-validator in separate terminal
        light test-validator
        ```

        <CodeGroup>
          ```typescript Action theme={null}
          import { Keypair } from "@solana/web3.js";
          import { createRpc } from "@lightprotocol/stateless.js";
          import { createMintInterface, createAtaInterface } from "@lightprotocol/compressed-token";

          async function main() {
          const rpc = createRpc();
          const payer = Keypair.generate();
          const sig = await rpc.requestAirdrop(payer.publicKey, 10e9);
          await rpc.confirmTransaction(sig);

          const { mint } = await createMintInterface(rpc, payer, payer, null, 9);

          const owner = Keypair.generate();
          const ata = await createAtaInterface(rpc, payer, mint, owner.publicKey);

          console.log("ATA:", ata.toBase58());
          }

          main().catch(console.error);

          ```

          ```typescript Instruction theme={null}
          import "dotenv/config";
          import { Keypair, ComputeBudgetProgram } from "@solana/web3.js";
          import { createRpc, buildAndSignTx, sendAndConfirmTx, CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js";
          import {
              createMintInterface,
              createAssociatedTokenAccountInterfaceInstruction,
              getAssociatedTokenAddressInterface,
          } from "@lightprotocol/compressed-token";
          import { homedir } from "os";
          import { readFileSync } from "fs";

          const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
          const payer = Keypair.fromSecretKey(
              new Uint8Array(
                  JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
              )
          );

          (async function () {
              const rpc = createRpc(RPC_URL);

              const { mint } = await createMintInterface(rpc, payer, payer, null, 9);

              const owner = Keypair.generate();
              const associatedToken = getAssociatedTokenAddressInterface(mint, owner.publicKey);

              const ix = createAssociatedTokenAccountInterfaceInstruction(
                  payer.publicKey,
                  associatedToken,
                  owner.publicKey,
                  mint,
                  CTOKEN_PROGRAM_ID
              );

              const { blockhash } = await rpc.getLatestBlockhash();
              const tx = buildAndSignTx(
                  [ComputeBudgetProgram.setComputeUnitLimit({ units: 100_000 }), ix],
                  payer,
                  blockhash
              );
              const signature = await sendAndConfirmTx(rpc, tx);

              console.log("ATA:", associatedToken.toBase58());
              console.log("Tx:", signature);
          })();
          ```
        </CodeGroup>
      </Step>
    </Steps>
  </Tab>

  <Tab title="Rust Client">
    1. The example creates a test light-mint. You can use existing light-mints, SPL or Token 2022 mints as well.
    2. Derive the address from mint and owner pubkey.
    3. Build the instruction with `CreateAssociatedCTokenAccount`. It automatically includes the default rent config:

    ```rust theme={null}
    use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccount;

    let instruction = CreateAssociatedCTokenAccount::new(
        payer.pubkey(),
        owner,
        mint,
    )
    .instruction()?;
    ```

    4. Send transaction & verify light-ATA creation with `get_account`.

    <Steps>
      <Step>
        ### Prerequisites

        <Accordion title="Dependencies">
          ```toml Cargo.toml theme={null}
          [dependencies]
          light-compressed-token-sdk = "0.1"
          light-client = "0.1"
          light-ctoken-types = "0.1"
          solana-sdk = "2.2"
          borsh = "0.10"
          tokio = { version = "1.36", features = ["full"] }

          [dev-dependencies]
          light-program-test = "0.1"  # For in-memory tests with LiteSVM
          ```
        </Accordion>

        <Accordion title="Developer Environment">
          <Tabs>
            <Tab title="In-Memory (LightProgramTest)">
              Test with Lite-SVM (...)

              ```bash theme={null}
              # Initialize project
              cargo init my-light-project
              cd my-light-project

              # Run tests
              cargo test
              ```

              ```rust theme={null}
              use light_program_test::{LightProgramTest, ProgramTestConfig};
              use solana_sdk::signer::Signer;

              #[tokio::test]
              async fn test_example() {
                  // In-memory test environment 
                  let mut rpc = LightProgramTest::new(ProgramTestConfig::default())
                      .await
                      .unwrap();

                  let payer = rpc.get_payer().insecure_clone();
                  println!("Payer: {}", payer.pubkey());
              }
              ```
            </Tab>

            <Tab title="Localnet (LightClient)">
              Connects to a local test validator.

              <Tabs>
                <Tab title="npm">
                  ```bash theme={null}
                  npm install -g @lightprotocol/zk-compression-cli@alpha
                  ```
                </Tab>

                <Tab title="yarn">
                  ```bash theme={null}
                  yarn global add @lightprotocol/zk-compression-cli@alpha
                  ```
                </Tab>

                <Tab title="pnpm">
                  ```bash theme={null}
                  pnpm add -g @lightprotocol/zk-compression-cli@alpha
                  ```
                </Tab>
              </Tabs>

              ```bash theme={null}
              # Initialize project
              cargo init my-light-project
              cd my-light-project

              # Start local test validator (in separate terminal)
              light test-validator
              ```

              ```rust theme={null}
              use light_client::rpc::{LightClient, LightClientConfig, Rpc};

              #[tokio::main]
              async fn main() -> Result<(), Box<dyn std::error::Error>> {
                  // Connects to http://localhost:8899
                  let rpc = LightClient::new(LightClientConfig::local()).await?;

                  let slot = rpc.get_slot().await?;
                  println!("Current slot: {}", slot);

                  Ok(())
              }
              ```
            </Tab>

            <Tab title="Devnet (LightClient)">
              Replace `<your-api-key>` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression).

              ```rust theme={null}
              use light_client::rpc::{LightClient, LightClientConfig, Rpc};

              #[tokio::main]
              async fn main() -> Result<(), Box<dyn std::error::Error>> {
                  let rpc_url = "https://devnet.helius-rpc.com?api-key=<your_api_key>";
                  let rpc = LightClient::new(
                      LightClientConfig::new(rpc_url.to_string(), None, None)
                  ).await?;

                  println!("Connected to Devnet");
                  Ok(())
              }
              ```
            </Tab>
          </Tabs>
        </Accordion>
      </Step>

      <Step>
        ### Create ATA

        ```rust theme={null}
        use borsh::BorshDeserialize;
        use light_client::indexer::{AddressWithTree, Indexer};
        use light_client::rpc::{LightClient, LightClientConfig, Rpc};
        use light_ctoken_sdk::ctoken::{
            derive_ctoken_ata, CreateAssociatedCTokenAccount, CreateCMint,
            CreateCMintParams,
        };
        use light_ctoken_interface::state::CToken;
        use serde_json;
        use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer};
        use std::convert::TryFrom;
        use std::env;
        use std::fs;

        #[tokio::test(flavor = "multi_thread")]
        async fn test_create_associated_token_account() {
            dotenvy::dotenv().ok();

            let keypair_path = env::var("KEYPAIR_PATH")
                .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap()));
            let payer = load_keypair(&keypair_path).expect("Failed to load keypair");

            let api_key = env::var("api_key")
                .expect("api_key environment variable must be set");

            let config = LightClientConfig::devnet(
                Some("https://devnet.helius-rpc.com".to_string()),
                Some(api_key),
            );
            let mut rpc = LightClient::new_with_retry(config, None)
                .await
                .expect("Failed to initialize LightClient");

            // Step 1: Create compressed mint (prerequisite)
            let (mint, _compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await;

            // Step 2: Define owner and derive ATA address
            let owner = payer.pubkey();
            let (ata_address, _bump) = derive_ctoken_ata(&owner, &mint);

            // Step 3: Build instruction using SDK builder
            let instruction = CreateAssociatedCTokenAccount::new(
                payer.pubkey(),
                owner,
                mint,
            )
            .instruction()
            .unwrap();

            // Step 4: Send transaction (only payer signs, no account keypair needed)
            rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer])
                .await
                .unwrap();

            // Step 5: Verify light-ATA creation
            let account_data = rpc.get_account(ata_address).await.unwrap().unwrap();
            let ctoken_state = CToken::deserialize(&mut &account_data.data[..]).unwrap();

            assert_eq!(ctoken_state.mint, mint.to_bytes(), "Mint should match");
            assert_eq!(ctoken_state.owner, owner.to_bytes(), "Owner should match");
            assert_eq!(ctoken_state.amount, 0, "Initial amount should be 0");
        }

        pub async fn create_compressed_mint<R: Rpc + Indexer>(
            rpc: &mut R,
            payer: &Keypair,
            decimals: u8,
        ) -> (Pubkey, [u8; 32]) {
            let mint_signer = Keypair::new();
            let address_tree = rpc.get_address_tree_v2();

            let _ = rpc.get_latest_active_state_trees().await;
            let output_pubkey = match rpc
                .get_random_state_tree_info()
                .ok()
                .or_else(|| rpc.get_random_state_tree_info_v1().ok())
            {
                Some(info) => info
                    .get_output_pubkey()
                    .expect("Invalid state tree type for output"),
                None => {
                    let queues = rpc
                        .indexer_mut()
                        .expect("IndexerNotInitialized")
                        .get_queue_info(None)
                        .await
                        .expect("Failed to fetch queue info")
                        .value
                        .queues;
                    queues
                        .get(0)
                        .map(|q| q.queue)
                        .expect("NoStateTreesAvailable")
                }
            };

            // Derive compression address
            let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address(
                &mint_signer.pubkey(),
                &address_tree.tree,
            );

            let mint_pda =
                light_ctoken_sdk::ctoken::find_cmint_address(&mint_signer.pubkey()).0;

            // Get validity proof for the address
            let rpc_result = rpc
                .get_validity_proof(
                    vec![],
                    vec![AddressWithTree {
                        address: compression_address,
                        tree: address_tree.tree,
                    }],
                    None,
                )
                .await
                .unwrap()
                .value;

            // Build params
            let params = CreateCMintParams {
                decimals,
                address_merkle_tree_root_index: rpc_result.addresses[0].root_index,
                mint_authority: payer.pubkey(),
                proof: rpc_result.proof.0.unwrap(),
                compression_address,
                mint: mint_pda,
                freeze_authority: None,
                extensions: None,
            };

            // Create instruction
            let create_cmint = CreateCMint::new(
                params,
                mint_signer.pubkey(),
                payer.pubkey(),
                address_tree.tree,
                output_pubkey,
            );
            let instruction = create_cmint.instruction().unwrap();

            // Send transaction
            rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_signer])
                .await
                .unwrap();

            (mint_pda, compression_address)
        }

        fn load_keypair(path: &str) -> Result<Keypair, Box<dyn std::error::Error>> {
            let path = if path.starts_with("~") {
                path.replace("~", &env::var("HOME").unwrap_or_default())
            } else {
                path.to_string()
            };
            let file = fs::read_to_string(&path)?;
            let bytes: Vec<u8> = serde_json::from_str(&file)?;
            Ok(Keypair::try_from(&bytes[..])?)
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Program Guide">
    <Note>
      Find [a full code example at the end](#full-code-example).
    </Note>

    <Steps>
      <Step>
        ### Define Rent Config Accounts

        ```rust theme={null}
        use light_compressed_token_sdk::ctoken::CompressibleParamsInfos;

        let compressible_params = CompressibleParamsInfos::new(
            compressible_config.clone(),
            rent_sponsor.clone(),
            system_program.clone(),
        );
        ```

        <table>
          <colgroup>
            <col style={{ width: "25%", textAlign: "left" }} />

            <col style={{ width: "55%" }} />
          </colgroup>

          <thead>
            <tr>
              <th style={{ textAlign: "left" }} />

              <th style={{ textAlign: "left" }} />
            </tr>
          </thead>

          <tbody>
            <tr>
              <td style={{ textAlign: "left" }}>
                <strong>
                  <Tooltip tip="Owned by LightRegistry program. Stores rent_sponsor, compression_delay, address_space, and rent_config.">
                    Compressible Config
                  </Tooltip>
                </strong>
              </td>

              <td>Protocol PDA that stores account rent config.</td>
            </tr>

            <tr>
              <td style={{ textAlign: "left" }}>
                <strong>
                  <Tooltip tip="light token program PDA that manages rent for compressible accounts.">
                    Rent Sponsor
                  </Tooltip>
                </strong>
              </td>

              <td>
                * light token program PDA that fronts rent exemption at creation.
                  <br />- Claims rent when account compresses.
              </td>
            </tr>

            <tr>
              <td style={{ textAlign: "left" }}>
                <strong>
                  <Tooltip tip="11111111111111111111111111111111" cta="Program ID" href="https://solscan.io/account/11111111111111111111111111111111">
                    System Program
                  </Tooltip>
                </strong>
              </td>

              <td>Solana System Program to create the on-chain account.</td>
            </tr>
          </tbody>
        </table>
      </Step>

      <Step>
        ### Build Account Infos and CPI the Compressed Token Program

        1. Pass the required accounts that include the rent config.
        2. Use `invoke` or `invoke_signed`, when a CPI requires a PDA signer.
                   <Note>
                     The light-ATA address is derived from `[owner, ctoken_program_id, mint]`.
                     Unlike light-token accounts, owner and mint are passed as accounts, not in
                     instruction data.
                   </Note>

        <Tabs>
          <Tab title="invoke (External signer)">
            ```rust theme={null}
            use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccountCpi;

            CreateAssociatedCTokenAccountCpi {
                owner: owner.clone(),
                mint: mint.clone(),
                payer: payer.clone(),
                associated_token_account: associated_token_account.clone(),
                system_program: system_program.clone(),
                bump: data.bump,
                compressible: Some(compressible_params),
                idempotent: false,
            }
            .invoke()?;
            ```
          </Tab>

          <Tab title="invoke_signed (PDA is signer)">
            ```rust theme={null}
            use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccountCpi;

            let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]];
            CreateAssociatedCTokenAccountCpi {
                owner: owner.clone(),
                mint: mint.clone(),
                payer: payer.clone(),
                associated_token_account: associated_token_account.clone(),
                system_program: system_program.clone(),
                bump: data.bump,
                compressible: Some(compressible_params),
                idempotent: false,
            }
            .invoke_signed(&[signer_seeds])?;
            ```
          </Tab>
        </Tabs>

        <table>
          <colgroup>
            <col style={{width: '25%', textAlign: 'left'}} />

            <col style={{width: '55%'}} />
          </colgroup>

          <thead>
            <tr>
              <th style={{textAlign: 'left'}} />

              <th style={{textAlign: 'left'}} />

              <th style={{textAlign: 'left'}} />
            </tr>
          </thead>

          <tbody>
            <tr>
              <td style={{textAlign: 'left'}}><strong>Owner</strong></td>
              <td>-</td>

              <td>
                * The wallet that will own this light-ATA.<br />
                * Used to derive the light-ATA address deterministically.
              </td>
            </tr>

            <tr>
              <td style={{textAlign: 'left'}}><strong>Mint</strong></td>
              <td>-</td>

              <td>
                * The SPL or light-mint token mint.<br />
                * Used to derive the light-ATA address deterministically.
              </td>
            </tr>

            <tr>
              <td style={{textAlign: 'left'}}><strong>Payer</strong></td>
              <td>signer, mutable</td>

              <td>
                * Pays initial rent per epoch, transaction fee and compression incentive.<br />
                * Does NOT pay rent exemption (fronted by `rent_sponsor`).
              </td>
            </tr>

            <tr>
              <td style={{textAlign: 'left'}}><strong>light-ATA Account</strong></td>
              <td>mutable</td>

              <td>
                * The light-ATA being created.<br />
                * Address is derived from `[owner, ctoken_program_id, mint]`.
              </td>
            </tr>

            <tr>
              <td style={{textAlign: 'left'}}><strong><Tooltip tip="11111111111111111111111111111111" cta="Program ID" href="https://solscan.io/account/11111111111111111111111111111111">System Program</Tooltip></strong></td>
              <td>-</td>
              <td>Solana System Program. Required for CPI to create the on-chain account.</td>
            </tr>

            <tr>
              <td style={{textAlign: 'left'}}><strong>Bump</strong></td>
              <td>u8</td>
              <td>The PDA bump seed for the light-ATA address derivation.</td>
            </tr>

            <tr>
              <td style={{textAlign: 'left'}}><strong>Idempotent</strong></td>
              <td>bool</td>

              <td>
                * When `true`, silently succeeds if account already exists.<br />
                * When `false`, fails if account already exists.
              </td>
            </tr>
          </tbody>
        </table>
      </Step>
    </Steps>

    # Full Code Example

    <Info>
      Find the source code
      [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-ctoken-test/src/create_ata2.rs).
    </Info>

    ```rust expandable theme={null}
    use borsh::{BorshDeserialize, BorshSerialize};
    use light_ctoken_sdk::ctoken::{CompressibleParamsCpi, CreateAssociatedCTokenAccountCpi};
    use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};

    use crate::{ATA_SEED, ID};

    /// Instruction data for create ATA V2 (owner/mint as accounts)
    #[derive(BorshSerialize, BorshDeserialize, Debug)]
    pub struct CreateAta2Data {
        pub bump: u8,
        pub pre_pay_num_epochs: u8,
        pub lamports_per_write: u32,
    }

    /// Handler for creating ATA using V2 variant (invoke)
    ///
    /// Account order:
    /// - accounts[0]: owner (readonly)
    /// - accounts[1]: mint (readonly)
    /// - accounts[2]: payer (signer, writable)
    /// - accounts[3]: associated_token_account (writable)
    /// - accounts[4]: system_program
    /// - accounts[5]: compressible_config
    /// - accounts[6]: rent_sponsor (writable)
    pub fn process_create_ata2_invoke(
        accounts: &[AccountInfo],
        data: CreateAta2Data,
    ) -> Result<(), ProgramError> {
        if accounts.len() < 7 {
            return Err(ProgramError::NotEnoughAccountKeys);
        }

        let compressible_params = CompressibleParamsCpi::new(
            accounts[5].clone(),
            accounts[6].clone(),
            accounts[4].clone(),
        );

        CreateAssociatedCTokenAccountCpi {
            owner: accounts[0].clone(),
            mint: accounts[1].clone(),
            payer: accounts[2].clone(),
            associated_token_account: accounts[3].clone(),
            system_program: accounts[4].clone(),
            bump: data.bump,
            compressible: Some(compressible_params),
            idempotent: false,
        }
        .invoke()?;

        Ok(())
    }

    /// Handler for creating ATA using V2 variant with PDA ownership (invoke_signed)
    ///
    /// Account order:
    /// - accounts[0]: owner (PDA, readonly)
    /// - accounts[1]: mint (readonly)
    /// - accounts[2]: payer (PDA, writable, not signer - program signs)
    /// - accounts[3]: associated_token_account (writable)
    /// - accounts[4]: system_program
    /// - accounts[5]: compressible_config
    /// - accounts[6]: rent_sponsor (writable)
    pub fn process_create_ata2_invoke_signed(
        accounts: &[AccountInfo],
        data: CreateAta2Data,
    ) -> Result<(), ProgramError> {
        if accounts.len() < 7 {
            return Err(ProgramError::NotEnoughAccountKeys);
        }

        // Derive the PDA that will act as payer
        let (pda, bump) = Pubkey::find_program_address(&[ATA_SEED], &ID);

        // Verify the payer is the PDA
        if &pda != accounts[2].key {
            return Err(ProgramError::InvalidSeeds);
        }

        let compressible_params = CompressibleParamsCpi::new(
            accounts[5].clone(),
            accounts[6].clone(),
            accounts[4].clone(),
        );

        let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]];
        CreateAssociatedCTokenAccountCpi {
            owner: accounts[0].clone(),
            mint: accounts[1].clone(),
            payer: accounts[2].clone(), // PDA
            associated_token_account: accounts[3].clone(),
            system_program: accounts[4].clone(),
            bump: data.bump,
            compressible: Some(compressible_params),
            idempotent: false,
        }
        .invoke_signed(&[signer_seeds])?;

        Ok(())
    }
    ```
  </Tab>
</Tabs>

# Next Steps

{" "}

<Card title="Learn how to mint light-tokens" icon="chevron-right" color="#0066ff" href="/light-token/cookbook/mint-to" horizontal />
