CHATSTARS
Multi-client Link Launcher

Create and manage one launcher per client.

Pick an existing creator, start a new one, publish their custom landing page, and keep every client's XLink rotation separate.

Clients

0 saved

Current

New client

Links

0 active

Client launcher editor

One saved profile = one client landing page + one rotating CTA setup.

Landing-page button links

Add the xli.ink links here. Destination 1 is used first. When the same visitor comes back and clicks again, destination 2 is used.

1
2

Preview

Save to enable live link
C

Brandy G

View More

Important setup note

This is now designed for multiple clients. Create one profile per creator/client, publish it, then attach that client's custom domain to the matching live URL.

Setup checklist

  1. 1Add client-domain.com to the Vercel project as a custom domain for this model.
  2. 2Point Cloudflare DNS to Vercel: A @ 76.76.21.21 and CNAME www cname.vercel-dns.com.
  3. 3After DNS verifies, client-domain.com will open this model's landing page directly.
  4. 4The landing-page CTA sends first-time clickers to xli.ink destination 1, then returning clickers to xli.ink destination 2.
  5. 5Set the model's bio link to client-domain.com; use https://chatstarlinks.com/x/client only as the platform preview URL.

Client routes

Live pageSave first to create a live URL
Redirect routeSave first to create a redirect URL

Optional Worker code

// Cloudflare Worker for yourdomain.com
// Routes:
//   /      -> landing page HTML
//   /go    -> smart redirect rotator
// Rotation mode: sequential

const LANDING_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Creator</title>
  <meta name="description" content="Official links" />
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
    .page { position: relative; min-height: 100vh; overflow: hidden; background: #ff2d8b; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 24px; box-sizing: border-box; }
    .page::before { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, #ff2d8b, #4a102f); transform: scale(1.13); transform-origin: 51% 100%; z-index: 0; }
    .page::after { content: ""; position: absolute; inset: 0; background: rgba(0,0,0,.35); z-index: 1; pointer-events: none; }
    .page > * { position: relative; z-index: 2; }
    .avatar { width: 170px; height: 170px; border-radius: 50%; border: 5px solid #fff; object-fit: cover; object-position: 50% 0%; margin-bottom: 18px; box-shadow: 0 4px 20px rgba(0,0,0,.25); background: rgba(255,255,255,.15); color: #fff; display: grid; place-items: center; overflow: hidden; font-size: 48px; font-weight: 600; }
    .avatar img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 0%; display: block; }
    .name { font-size: 30px; font-weight: 500; color: #fff; text-shadow: 0 1px 8px rgba(0,0,0,.5); margin: 0 0 36px; display: flex; align-items: center; justify-content: center; gap: 8px; }
    a { padding: 20px 44px; border-radius: 999px; font-size: 18px; font-weight: 500; cursor: pointer; min-width: 260px; font-family: inherit; background: rgba(255,255,255,.18); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid rgba(255,255,255,.35); color: #fff; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
    a:hover { opacity: .92; transform: translateY(-1px); transition: all .15s; }
  </style>
</head>
<body>
  <main class="page">
    <div class="avatar">C</div>
    <div class="name"><span>Brandy G</span><svg width="20" height="20" viewBox="0 0 24 24" style="flex-shrink:0;filter:drop-shadow(0 1px 2px rgba(0,0,0,.25))"><path fill="#3897f0" d="M23 12l-2.44-2.79.34-3.69-3.61-.82-1.89-3.2L12 2.96 8.6 1.5 6.71 4.69 3.1 5.5l.34 3.7L1 12l2.44 2.79-.34 3.7 3.61.82L8.6 22.5l3.4-1.47 3.4 1.46 1.89-3.19 3.61-.82-.34-3.69L23 12zm-12.91 4.72l-3.8-3.81 1.48-1.48 2.32 2.33 5.85-5.87 1.48 1.48-7.33 7.35z"/></svg></div>
    <a href="https://yourdomain.com/go">View More</a>
  </main>
</body>
</html>`;
const DESTINATIONS = [
  {
    "label": "Destination",
    "url": "https://example.com",
    "weight": 100
  }
];
const ROTATION_MODE = "sequential";

function pickWeighted() {
  const total = DESTINATIONS.reduce((sum, item) => sum + Math.max(0, Number(item.weight || 0)), 0) || DESTINATIONS.length;
  let cursor = Math.random() * total;
  for (const item of DESTINATIONS) {
    cursor -= Math.max(0, Number(item.weight || 0)) || 1;
    if (cursor <= 0) return item;
  }
  return DESTINATIONS[0];
}

function pickDestination(request) {
  const cookie = request.headers.get("Cookie") || "";
  const match = cookie.match(/launcher_clicks=(\d+)/);
  const clickCount = match ? Number(match[1]) : 0;
  if (ROTATION_MODE === "weighted") return { destination: pickWeighted(), nextCount: clickCount + 1 };
  const index = clickCount % DESTINATIONS.length;
  return { destination: DESTINATIONS[index], nextCount: clickCount + 1 };
}

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/go") {
      const { destination, nextCount } = pickDestination(request);
      const response = Response.redirect(destination.url, 302);
      response.headers.append("Set-Cookie", "launcher_clicks=" + nextCount + "; Path=/; Max-Age=2592000; SameSite=Lax; Secure");
      response.headers.set("Cache-Control", "no-store");
      return response;
    }
    return new Response(LANDING_HTML, { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=60" } });
  },
};