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

# Display a presale

> Add presale totals and dates to your website.

Paste this into an HTML page and replace `YOUR_VAULT_ADDRESS`. It displays precommitted funds, deposits and dates, and refreshes every 15 seconds. No API key or SDK needed.

```html theme={null}
<div id="presale">Loading presale…</div>
<p id="presale-status"></p>

<script type="module">
  const vault = 'YOUR_VAULT_ADDRESS';
  const output = document.querySelector('#presale');
  const status = document.querySelector('#presale-status');

  // Format token amounts without losing precision.
  function tokens(atoms, decimals) {
    const digits = BigInt(atoms)
      .toString()
      .padStart(decimals + 1, '0');
    if (decimals === 0) return digits;
    const fraction = digits.slice(-decimals).replace(/0+$/, '');
    return digits.slice(0, -decimals) + (fraction ? '.' + fraction : '');
  }
  const date = (seconds) =>
    seconds == null ? 'Not scheduled' : new Date(seconds * 1000).toLocaleString();

  async function refresh() {
    try {
      const response = await fetch(
        `https://indexer.star.fun/api/vaults/${encodeURIComponent(vault)}/fundraise`,
        { cache: 'no-store' },
      );
      if (!response.ok) throw new Error('Could not load presale');
      const data = await response.json();
      output.replaceChildren(
        ...[
          `Phase: ${data.on_chain_phase}`,
          `Precommitted: ${tokens(data.amounts.precommit_locked, data.quote_decimals)}`,
          `Deposited: ${tokens(data.amounts.deposited, data.quote_decimals)}`,
          `Starts: ${date(data.start_time_seconds)}`,
          `Ends: ${date(data.end_time_seconds)}`,
        ].map((text) => {
          const line = document.createElement('p');
          line.textContent = text;
          return line;
        }),
      );
      status.textContent = data.freshness.status === 'healthy' ? '' : 'Updates delayed';
    } catch {
      status.textContent = 'Updates unavailable. Retrying…';
    } finally {
      setTimeout(refresh, 15_000);
    }
  }
  refresh();
</script>
```

Amounts are in the contribution token (`quote_mint`). Label them with that token's symbol in your UI.

## Fields you need

| Field                                    | Meaning                                                                    |
| ---------------------------------------- | -------------------------------------------------------------------------- |
| `amounts.precommit_locked`               | Funds still held in precommit escrow                                       |
| `amounts.deposited`                      | Deposits credited to the raise, including converted precommits, after fees |
| `quote_decimals`                         | Decimal places used to display amounts                                     |
| `on_chain_phase`                         | `precommit_active` before the raise; `active` during fundraising           |
| `start_time_seconds`, `end_time_seconds` | Unix seconds; `null` before the raise starts                               |

Keep precommits and deposits separate. An `active` phase can remain after the end time; check the deadline before enabling deposits.

To let users contribute, follow the [presale SDK guide](/developers/presale-sdk).

<Accordion title="Optional: WebSocket updates">
  Connect to `wss://indexer.star.fun/ws`. It broadcasts all vaults automatically; there is no subscribe message. Match the network, program and vault from your initial snapshot:

  ```ts theme={null}
  async function fetchFundraise(vaultAddress: string) {
    const response = await fetch(
      `https://indexer.star.fun/api/vaults/${encodeURIComponent(vaultAddress)}/fundraise`,
      { cache: 'no-store' },
    );
    if (!response.ok) throw new Error(`Snapshot unavailable: ${response.status}`);
    return response.json();
  }
  let snapshot = await fetchFundraise(VAULT_ADDRESS);
  const expected = snapshot;
  const socket = new WebSocket('wss://indexer.star.fun/ws');
  let refreshing = false;

  async function refresh() {
    if (refreshing) return;
    refreshing = true;
    try {
      const next = await fetchFundraise(expected.vault_address);
      if (
        next.genesis_hash !== expected.genesis_hash ||
        next.program_id !== expected.program_id ||
        next.vault_address !== expected.vault_address
      )
        return;
      if (BigInt(next.revision) >= BigInt(snapshot.revision)) {
        snapshot = next; // Update your component's state here.
      }
    } catch {
      // Keep the last values and show that updates are unavailable.
    } finally {
      refreshing = false;
    }
  }

  socket.addEventListener('open', refresh);
  socket.addEventListener('message', ({ data }) => {
    let event;
    try {
      event = JSON.parse(data);
    } catch {
      return;
    }
    if (
      event?.type === 'fundraise_snapshot_updated' &&
      event.schema_version === 1 &&
      event.genesis_hash === expected.genesis_hash &&
      event.program_id === expected.program_id &&
      event.vault_address === expected.vault_address
    )
      void refresh();
  });
  const timer = setInterval(refresh, 15_000);
  // On unmount: clearInterval(timer); socket.close();
  ```

  Notifications are refresh signals, not balances or transaction receipts. Keep polling, refresh after confirmed transactions, and reconnect with backoff. Debounce notifications and discard late responses when switching vaults.

  Snapshots reflect finalized state and can lag a confirmed transaction. Accept equal revisions: freshness can improve without a balance change. Show `stale` or `unknown` visibly and preserve the last values on errors. A `503` means the snapshot is unavailable, not zero; retry after the supplied `Retry-After` delay. Back off on `429` rate limits.
</Accordion>
