> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rabbithole.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Why Rabbithole

> Curated incentive campaigns with per-deposit attribution: see how much TVL was routed, where it came from, and what your budget bought

export const RetentionChart = () => {
  const DAYS = 90;
  const END = 60;
  const PLAY_MS = 2600;
  const W = 1000;
  const H = 420;
  const PAD_X = 8;
  const PAD_TOP = 24;
  const PAD_BOTTOM = 30;
  const INNER_W = W - PAD_X * 2;
  const INNER_H = H - PAD_TOP - PAD_BOTTOM;
  const MAX = 26000000;
  const qualified = [{
    day: 0,
    usd: 8000000
  }, {
    day: 15,
    usd: 14000000
  }, {
    day: 30,
    usd: 19000000
  }, {
    day: 45,
    usd: 22000000
  }, {
    day: 60,
    usd: 24000000
  }, {
    day: 75,
    usd: 22000000
  }, {
    day: 90,
    usd: 21000000
  }];
  const unqualified = [{
    day: 0,
    usd: 8000000
  }, {
    day: 15,
    usd: 14000000
  }, {
    day: 30,
    usd: 20000000
  }, {
    day: 45,
    usd: 23000000
  }, {
    day: 60,
    usd: 26000000
  }, {
    day: 75,
    usd: 11000000
  }, {
    day: 90,
    usd: 6000000
  }];
  const px = day => PAD_X + day / DAYS * INNER_W;
  const py = usd => {
    const frac = 1 - usd / MAX;
    return PAD_TOP + frac * INNER_H;
  };
  const linePath = pts => pts.map((p, i) => {
    const cmd = i === 0 ? "M" : "L";
    return cmd + " " + px(p.day) + " " + py(p.usd);
  }).join(" ");
  const areaPath = pts => {
    const last = pts[pts.length - 1];
    const first = pts[0];
    const floor = H - PAD_BOTTOM;
    return linePath(pts) + " L " + px(last.day) + " " + floor + " L " + px(first.day) + " " + floor + " Z";
  };
  const valueAt = (pts, day) => {
    let prev = pts[0];
    for (const p of pts) {
      if (p.day >= day) {
        const span = p.day - prev.day;
        if (span === 0) return p.usd;
        const dv = p.usd - prev.usd;
        const dd = day - prev.day;
        return prev.usd + dv * dd / span;
      }
      prev = p;
    }
    return pts[pts.length - 1].usd;
  };
  const money = usd => {
    const m = usd / 1000000;
    return "$" + m.toFixed(0) + "M";
  };
  const pct = (v, total) => {
    const p = v / total * 100;
    return p + "%";
  };
  const chartRef = useRef(null);
  const dayRef = useRef(null);
  const qualRef = useRef(null);
  const baseRef = useRef(null);
  const clipRef = useRef(null);
  const markerRef = useRef(null);
  const dotRef = useRef(null);
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const el = chartRef.current;
    if (!el) return;
    if (typeof IntersectionObserver === "undefined") return;
    const apply = day => {
      const qual = valueAt(qualified, day);
      const base = valueAt(unqualified, day);
      if (clipRef.current) clipRef.current.setAttribute("width", String(px(day) + 2));
      if (dayRef.current) dayRef.current.textContent = String(Math.round(day)).padStart(2, "0");
      if (qualRef.current) qualRef.current.textContent = money(qual);
      if (baseRef.current) baseRef.current.textContent = money(base);
      if (markerRef.current) markerRef.current.setAttribute("opacity", day >= END ? "1" : "0.35");
      if (dotRef.current) {
        dotRef.current.style.left = pct(px(day), W);
        dotRef.current.style.top = pct(py(qual), H);
      }
    };
    apply(0);
    let frame = 0;
    let started = 0;
    const easeOut = t => 1 - Math.pow(1 - t, 3);
    const tick = now => {
      if (!started) started = now;
      const elapsed = now - started;
      const t = Math.min(1, elapsed / PLAY_MS);
      apply(easeOut(t) * DAYS);
      if (t < 1) frame = requestAnimationFrame(tick);
    };
    const observer = new IntersectionObserver(entries => {
      if (!entries[0].isIntersecting) return;
      observer.disconnect();
      frame = requestAnimationFrame(tick);
    }, {
      threshold: 0.4
    });
    observer.observe(el);
    return () => {
      observer.disconnect();
      if (frame) cancelAnimationFrame(frame);
    };
  }, []);
  const finalQual = valueAt(qualified, DAYS);
  const finalBase = valueAt(unqualified, DAYS);
  const dotLeft = pct(px(DAYS), W);
  const dotTop = pct(py(finalQual), H);
  const markerLeft = pct(px(END), W);
  const endX = px(END);
  const viewBox = "0 0 " + W + " " + H;
  const grid = [0.25, 0.5, 0.75];
  const summary = "Two cohorts over 90 days. Qualified capital rises to 24 million dollars by day 60 and holds near 21 million by day 90. Unqualified capital rises to 26 million by day 60 and falls to 6 million by day 90. Illustrative model, not observed performance.";
  return <div className="rh-anim not-prose">
    <dl className="rh-hud">
      <div>
        <dt className="rh-label">Day</dt>
        <dd className="rh-value"><span ref={dayRef}>{DAYS}</span><span style={{
    color: "var(--rh-text-3)"
  }}>/{DAYS}</span></dd>
      </div>
      <div>
        <dt className="rh-label rh-label--positive">Qualified</dt>
        <dd className="rh-value rh-value--positive"><span ref={qualRef}>{money(finalQual)}</span></dd>
      </div>
      <div>
        <dt className="rh-label rh-label--muted">Unqualified</dt>
        <dd className="rh-value rh-value--muted"><span ref={baseRef}>{money(finalBase)}</span></dd>
      </div>
    </dl>
    <div ref={chartRef} className="rh-chart">
      <svg viewBox={viewBox} preserveAspectRatio="none" role="img" aria-label={summary}>
        <defs>
          <linearGradient id="rh-qual-fill" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--rh-accent)" stopOpacity="0.22" />
            <stop offset="100%" stopColor="var(--rh-accent)" stopOpacity="0" />
          </linearGradient>
          <clipPath id="rh-time-clip">
            <rect ref={clipRef} x="0" y="0" width={W} height={H} />
          </clipPath>
        </defs>
        {grid.map(f => <line key={f} x1={PAD_X} x2={W - PAD_X} y1={PAD_TOP + f * INNER_H} y2={PAD_TOP + f * INNER_H} stroke="var(--rh-grid)" strokeWidth="1" vectorEffect="non-scaling-stroke" />)}
        <g ref={markerRef} opacity="1">
          <line x1={endX} x2={endX} y1={PAD_TOP - 8} y2={H - PAD_BOTTOM} stroke="var(--rh-muted-stroke)" strokeWidth="1" strokeDasharray="4 4" vectorEffect="non-scaling-stroke" />
        </g>
        <g clipPath="url(#rh-time-clip)">
          <path d={areaPath(qualified)} fill="url(#rh-qual-fill)" />
          <path d={linePath(unqualified)} fill="none" stroke="var(--rh-muted-stroke)" strokeWidth="1.5" strokeDasharray="5 5" vectorEffect="non-scaling-stroke" />
          <path d={linePath(qualified)} fill="none" stroke="var(--rh-accent)" strokeWidth="2.5" vectorEffect="non-scaling-stroke" />
        </g>
      </svg>
      <div ref={dotRef} aria-hidden className="rh-chart__dot" style={{
    left: dotLeft,
    top: dotTop
  }} />
      <span aria-hidden className="rh-label rh-chart__marker" style={{
    left: markerLeft
  }}>Incentives end</span>
    </div>
    <div className="rh-legend">
      <div className="rh-legend__item">
        <span aria-hidden className="rh-legend__swatch" />
        <div>
          <p className="rh-legend__title">Qualified capital</p>
          <p className="rh-body rh-legend__text">Targeted deposits. They rise with the campaign and hold after incentives end.</p>
        </div>
      </div>
      <div className="rh-legend__item">
        <span aria-hidden className="rh-legend__swatch rh-legend__swatch--dashed" />
        <div>
          <p className="rh-legend__title">Unqualified capital</p>
          <p className="rh-body rh-legend__text">Farms the reward, then leaves the moment incentives stop.</p>
        </div>
      </div>
    </div>
  </div>;
};

export const RewardModes = () => {
  const W = 148;
  const H = 56;
  const STROKE = "var(--rh-accent)";
  const MUTED = "var(--rh-muted-stroke)";
  const y8 = H - 8;
  const y14 = H - 14;
  const y20 = H - 20;
  const y28 = H - 28;
  const y34 = H - 34;
  const y42 = H - 42;
  const axisY = H - 6;
  const cliffX = W * 0.55;
  const msA = W * 0.33;
  const msB = W * 0.66;
  const modes = [{
    id: "time-weighted",
    name: "Time-weighted",
    mechanic: "Accrual scales with time in position.",
    detail: "Longer in the pool, longer it earns."
  }, {
    id: "cliffs",
    name: "Cliffs",
    mechanic: "Nothing vests until the cliff day.",
    detail: "Zero through the lock, then everything at once."
  }, {
    id: "milestones",
    name: "Milestones",
    mechanic: "Bonuses unlock at named days.",
    detail: "Step bonuses at day 30 and day 60."
  }, {
    id: "net-new",
    name: "New capital",
    mechanic: "Rotated capital does not qualify.",
    detail: "Only balance new to the position accrues."
  }, {
    id: "caps",
    name: "Caps",
    mechanic: "Ceilings per wallet and for the whole campaign.",
    detail: "No single depositor can take the budget."
  }];
  const paths = {
    "time-weighted": "M0 " + y8 + " C " + W * 0.4 + " " + y14 + ", " + W * 0.6 + " 22, " + W + " 8",
    cliffs: "M0 " + y8 + " L " + cliffX + " " + y8 + " L " + cliffX + " 12 L " + W + " 12",
    milestones: "M0 " + y8 + " L " + msA + " " + y20 + " L " + msA + " " + y28 + " L " + msB + " " + y34 + " L " + msB + " " + y42 + " L " + W + " 10",
    caps: "M0 " + y8 + " C " + W * 0.3 + " " + y20 + ", " + W * 0.4 + " 14, " + W * 0.62 + " 14 L " + W + " 14"
  };
  const [active, setActive] = useState(0);
  const [drawn, setDrawn] = useState(false);
  useEffect(() => {
    setDrawn(false);
    const raf = requestAnimationFrame(() => requestAnimationFrame(() => setDrawn(true)));
    const failsafe = window.setTimeout(() => setDrawn(true), 400);
    return () => {
      cancelAnimationFrame(raf);
      window.clearTimeout(failsafe);
    };
  }, [active]);
  const shape = id => {
    if (id === "net-new") {
      return <g><rect className="gd-mark gd-pulse" x={18} y={16} width={38} height={H - 24} fill={STROKE} rx={2} /><rect className="gd-mark" x={80} y={16} width={38} height={H - 24} fill="none" stroke={MUTED} strokeWidth={1.5} strokeDasharray="3 3" rx={2} /></g>;
    }
    if (id === "cliffs") {
      return <g><line className="gd-line" pathLength={1} x1={cliffX} y1={4} x2={cliffX} y2={axisY} stroke={MUTED} strokeWidth={1} strokeDasharray="3 3" /><path className="gd-line" pathLength={1} d={paths.cliffs} fill="none" stroke={STROKE} strokeWidth={2} /></g>;
    }
    if (id === "milestones") {
      return <g><path className="gd-line" pathLength={1} d={paths.milestones} fill="none" stroke={STROKE} strokeWidth={2} /><circle className="gd-mark" cx={msA} cy={y28} r={3} fill={STROKE} /><circle className="gd-mark" cx={msB} cy={y42} r={3} fill={STROKE} /></g>;
    }
    if (id === "caps") {
      return <g><line className="gd-line" pathLength={1} x1={0} y1={14} x2={W} y2={14} stroke={MUTED} strokeWidth={1} strokeDasharray="3 3" /><path className="gd-line" pathLength={1} d={paths.caps} fill="none" stroke={STROKE} strokeWidth={2} /></g>;
    }
    return <path className="gd-line" pathLength={1} d={paths["time-weighted"]} fill="none" stroke={STROKE} strokeWidth={2} />;
  };
  const viewBox = "0 0 " + W + " " + H;
  return <div className="rh-anim not-prose">
    <div className="rh-modes">
      {modes.map((mode, index) => {
    const open = index === active;
    const panelId = "rh-mode-" + mode.id;
    const label = "Accrual shape for " + mode.name + ": " + mode.mechanic;
    return <button key={mode.id} type="button" onClick={() => setActive(index)} aria-expanded={open} aria-controls={panelId} className="rh-mode" style={{
      flexGrow: open ? 1.6 : 1
    }}>
          <span className="rh-mode__name">{mode.name}</span>
          <span className="rh-mode__mechanic">{mode.mechanic}</span>
          <div id={panelId} aria-hidden={!open} className="rh-mode__panel">
            <div>
              <div className="rh-mode__inner">
                {open ? <svg viewBox={viewBox} data-draw={drawn ? "on" : "off"} className="rh-mode__diagram" role="img" aria-label={label}><line x1={0} y1={axisY} x2={W} y2={axisY} stroke={MUTED} strokeWidth={1} />{shape(mode.id)}</svg> : null}
                <span className="rh-mode__detail">{mode.detail}</span>
              </div>
            </div>
          </div>
        </button>;
  })}
    </div>
    <p className="rh-modes-note">Got a mechanic that is not here? We build it.</p>
  </div>;
};

export const CampaignWorkflow = () => {
  const steps = [{
    number: "01",
    title: "Define",
    body: "The pool, the chain, the asset you want TVL in, and how much.",
    control: "You set"
  }, {
    number: "02",
    title: "Target",
    body: "Who qualifies: net-new capital only, balance floors, wallet caps, onchain activity.",
    control: "You set"
  }, {
    number: "03",
    title: "Attribute",
    body: "We build and run the campaign in the app, and every deposit is traced to its source. You see what your budget bought, per campaign. Unclaimed budget returns to you.",
    control: "Rabbithole operates and reports"
  }];
  const listRef = useRef(null);
  const fillRef = useRef(null);
  const litRef = useRef(3);
  const [lit, setLit] = useState(3);
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const list = listRef.current;
    const fill = fillRef.current;
    if (!list || !fill) return;
    litRef.current = 0;
    setLit(0);
    let frame = 0;
    const step = () => {
      frame = 0;
      const rect = list.getBoundingClientRect();
      const read = window.innerHeight * 0.66 - rect.top;
      const progress = Math.min(1, Math.max(0, read / rect.height));
      fill.style.transform = "scaleY(" + progress + ")";
      let count = 0;
      list.querySelectorAll("[data-step]").forEach(node => {
        if (node.offsetTop + 14 <= read) count += 1;
      });
      if (count !== litRef.current) {
        litRef.current = count;
        setLit(count);
      }
    };
    const onScroll = () => {
      if (!frame) frame = requestAnimationFrame(step);
    };
    onScroll();
    window.addEventListener("scroll", onScroll, {
      passive: true
    });
    window.addEventListener("resize", onScroll, {
      passive: true
    });
    return () => {
      if (frame) cancelAnimationFrame(frame);
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, []);
  return <div className="rh-anim not-prose">
    <ol ref={listRef} className="rh-timeline">
      <span aria-hidden className="rh-timeline__spine" />
      <span ref={fillRef} aria-hidden className="rh-timeline__fill" style={{
    transform: "scaleY(1)"
  }} />
      {steps.map((step, index) => <li key={step.number} data-step data-on={index < lit ? "true" : "false"} className="rh-step">
        <span aria-hidden className="rh-step__node" />
        <div className="rh-step__body">
          <div className="rh-step__head">
            <span className="rh-label rh-label--muted">{step.number}</span>
            <h3 className="rh-step__title">{step.title}</h3>
          </div>
          <p className="rh-body rh-step__text">{step.body}</p>
          <span className="rh-label rh-label--muted rh-step__control">{step.control}</span>
        </div>
      </li>)}
    </ol>
  </div>;
};

Most incentive programs pay for deposits and hope. Rabbithole campaigns are curated, targeted, and attributed, so you can see exactly what your budget bought and keep what it didn't.

## The Difference

| Dimension           | Others                        | Rabbithole                             |
| ------------------- | ----------------------------- | -------------------------------------- |
| TVL attribution     | None                          | Per deposit                            |
| Campaign visibility | One of many                   | Curated, limited slots                 |
| Cohorts             | Whoever shows up              | Targeted by wallet, behaviour, balance |
| Audience data       | Unknown                       | Full cohort reporting, by region       |
| Strategy            | You design it                 | We design it with you                  |
| Deposits            | Bridge, swap, deposit by hand | One click, cross-chain swaps built in  |
| Unclaimed budget    | Varies by program             | Returns to you                         |

## Define. Target. Attribute.

You pick the pool. We route the TVL, and trace every deposit back to its source.

<CampaignWorkflow />

The full onboarding flow, from first conversation to deployment, is on [Launch a Campaign](/campaigns/launch-a-campaign).

## Strategies You Can't Run Alone

Accrual curves, cliffs, caps, and eligibility rules that would take your own engineering team to build. Pick the shape, we run it. Modes combine.

<RewardModes />

<Note>
  These are the shapes at a glance. The exact rules, restrictions, and how modes stack are documented in [Campaign Modes](/campaigns/campaign-modes).
</Note>

## What Every Campaign Ships With

<CardGroup cols={3}>
  <Card title="User base" icon="users">
    Distribution through the Rabbithole app. Your campaign reaches people who already deposit onchain.
  </Card>

  <Card title="User perks" icon="gift">
    Users earn more than your reward. Raffles, base rewards, and app perks push them to act.
  </Card>

  <Card title="One-click deposits" icon="bolt">
    Your users swap, bridge, and deposit from any token in one confirmation. Non-custodial throughout.
  </Card>

  <Card title="Reporting dashboard" icon="chart-column">
    Every team gets analytics. Track deposits, retention, and spend as the campaign runs.
  </Card>

  <Card title="No-loss" icon="rotate-left">
    Unclaimed rewards return to you.
  </Card>

  <Card title="SDK and API" icon="code" href="/developers/overview">
    Deposit and claim directly from your app.
  </Card>
</CardGroup>

## Growth That Sticks

Driving TVL is the first job. Keeping it is the one most programs never measure. We report retained capital at day 30, 60, and 90, so you know whether the growth held.

<RetentionChart />

<Note>
  **Illustrative model, not live performance.** The chart shows how the reporting model separates qualified capital, which is targeted and holds after incentives end, from unqualified capital, which farms the reward and leaves the moment incentives stop. How retention is defined and reported is on [Methodology](/campaigns/methodology).
</Note>

## Tell Us Where You Want TVL

We will build the campaign around it, run it, and show you exactly what it bought.

<Card title="Build a campaign" icon="rocket" href="/campaigns/launch-a-campaign">
  What to bring, what we set up together, and how deployment works.
</Card>
