AI-Powered Dating
Let Your AI Agent Find
Your Match
Your AI meets theirs first, uncovering the chemistry before a single hello is exchanged.


Real people. Real connections. AI-powered introductions.
How Prelude Works
A Smarter Way to Connect
Three simple steps to meaningful matches—powered by AI.
Create Your Profile
Tell us about yourself and what you’re looking for. Your AI agent learns your personality and preferences.
AI Agents Connect
Your AI agent chats with potential matches’ agents—finding shared interests, values, and chemistry.
Meet Your Match
When agents find a genuine connection, you get introduced with everything you need to start a great first date.
Why Choose Us
Why Prelude?
Skip the Small Talk
Your AI agent handles the awkward intros so every first message is already meaningful.
Deeper Compatibility
AI-driven matching goes beyond photos—analyzing communication style, values, and shared goals.
Privacy First
Your data stays yours. AI agents share only what you approve, keeping your real identity safe until you are ready.
AI-Powered Insight
Your Match Score
After your AI agents finish their date, you receive a detailed compatibility score — so you know exactly why you will click.
A score above 8.5 means you share strong alignment on the things that matter most.
Safety & Privacy
Safe by Design,
Built for Real Connections
We believe meaningful connections start with trust. Prelude protects your privacy so you can focus on what matters—real conversations.
Private by Design
End-to-end encryption keeps your conversations private and secure—always.
You Control What’s Shared
Decide exactly what your AI agent can access or delete anytime.
Verified Profiles
Real people, real intentions. We verify to keep Prelude authentic and safe.
Download Prelude
Available on iOS and Android.
Scan the QR code or tap below to get started.
/*! * Prelude living ribbon background. * * Technical starting point: adapted from Bojan Sehovac's animated canvas * ribbon/wave CodePen: https://codepen.io/bsehovac/pen/LQVzxJ * CodePen public Pens are available under the MIT license unless otherwise * noted: https://blog.codepen.io/documentation/licensing/ * * This version is bundled locally and rewritten for Prelude's approved * right-side ribbon composition, colours, reduced-motion fallback, resize * handling, and a single animation loop. No CodePen production files are * hotlinked. */ (function() { "use strict";
var root = document.getElementById("prelude-stage-root"); var canvas = root && root.querySelector("#prelude-ribbon-canvas"); if (!root || !canvas) return;
var ctx = canvas.getContext("2d", { alpha: true }); if (!ctx) return;
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)"); var raf = 0; var active = false; var dpr = 1; var size = { w: 1, h: 1 }; var mobile = false;
var controls = { // Colour, opacity, width, and speed controls for the living ribbon. speed: 0.28, drift: 4.2, textureSpeed: 9, textureOpacity: 0.56, glow: 0.32, ribbonWidth: 930, ribbonRight: 54, ribbonTop: -116 };
var bands = [ { name: "blue", offset: -165, width: 178, alpha: 0.72, amp: 8, phase: 0.2, glow: "rgba(172,210,255,.32)", stops: [ [0, "rgba(232,245,255,.06)"], [0.22, "rgba(204,226,255,.52)"], [0.66, "rgba(174,164,255,.42)"], [1, "rgba(234,246,255,.14)"] ] }, { name: "lavender", offset: -86, width: 150, alpha: 0.42, amp: 7, phase: 1.6, glow: "rgba(160,122,255,.28)", stops: [ [0, "rgba(218,208,255,.08)"], [0.28, "rgba(188,165,255,.5)"], [0.7, "rgba(141,90,246,.5)"], [1, "rgba(210,198,255,.16)"] ] }, { name: "violet", offset: -20, width: 150, alpha: 0.58, amp: 6, phase: 2.9, glow: "rgba(126,70,238,.28)", stops: [ [0, "rgba(151,115,255,.1)"], [0.38, "rgba(133,76,238,.48)"], [0.78, "rgba(123,79,239,.42)"], [1, "rgba(177,145,255,.16)"] ] }, { name: "orange", offset: 48, width: 144, alpha: 0.9, amp: 5.5, phase: 4.1, glow: "rgba(255,166,29,.42)", stops: [ [0, "rgba(255,222,139,.1)"], [0.3, "rgba(255,154,18,.9)"], [0.66, "rgba(255,118,47,.74)"], [1, "rgba(255,199,91,.22)"] ] }, { name: "gold", offset: 100, width: 84, alpha: 0.62, amp: 4, phase: 5.6, glow: "rgba(255,218,104,.46)", stops: [ [0, "rgba(255,246,204,.18)"], [0.42, "rgba(255,219,105,.76)"], [0.78, "rgba(255,166,48,.4)"], [1, "rgba(255,243,196,.16)"] ] }, { name: "coral", offset: 132, width: 96, alpha: 0.34, amp: 5.5, phase: 6.8, glow: "rgba(255,105,104,.32)", stops: [ [0, "rgba(255,145,119,.08)"], [0.34, "rgba(255,104,85,.58)"], [0.75, "rgba(255,122,124,.5)"], [1, "rgba(255,188,148,.14)"] ] }, { name: "pink", offset: 184, width: 142, alpha: 0.78, amp: 6.5, phase: 8, glow: "rgba(247,87,188,.36)", stops: [ [0, "rgba(255,118,191,.08)"], [0.36, "rgba(245,88,183,.68)"], [0.73, "rgba(244,98,197,.58)"], [1, "rgba(255,128,158,.18)"] ] } ];
var bandSegments = { blue: makeSegments([ [150, -122, 220, 58, 318, 218, 470, 358], [470, 358, 600, 480, 654, 640, 588, 818], [588, 818, 536, 956, 614, 1060, 724, 1208] ]), lavender: makeSegments([ [246, -120, 296, 70, 386, 236, 526, 376], [526, 376, 666, 516, 678, 670, 606, 838], [606, 838, 556, 956, 620, 1068, 738, 1212] ]), violet: makeSegments([ [300, -116, 338, 72, 426, 236, 576, 374], [576, 374, 710, 498, 716, 666, 640, 838], [640, 838, 586, 960, 630, 1074, 750, 1218] ]), orange: makeSegments([ [330, -116, 350, 78, 432, 246, 576, 382], [576, 382, 718, 516, 708, 682, 634, 846], [634, 846, 578, 970, 628, 1078, 748, 1218] ]), gold: makeSegments([ [392, -106, 382, 82, 450, 248, 590, 384], [590, 384, 724, 514, 716, 672, 646, 832], [646, 832, 592, 956, 616, 1072, 728, 1210] ]), coral: makeSegments([ [420, -104, 412, 78, 472, 246, 604, 388], [604, 388, 724, 518, 728, 660, 660, 826], [660, 826, 604, 958, 626, 1078, 744, 1214] ]), pink: makeSegments([ [470, -94, 438, 82, 488, 246, 628, 386], [628, 386, 744, 502, 748, 646, 684, 802], [684, 802, 626, 944, 642, 1068, 760, 1208] ]) };
var strandSegments = [ makeSegments([ [170, -120, 246, 58, 354, 216, 496, 354], [496, 354, 620, 476, 656, 634, 584, 806], [584, 806, 528, 942, 612, 1062, 724, 1200] ]), makeSegments([ [214, -118, 274, 60, 368, 226, 512, 366], [512, 366, 642, 492, 666, 640, 598, 818], [598, 818, 548, 954, 616, 1064, 730, 1206] ]), makeSegments([ [354, -112, 372, 74, 446, 246, 584, 388], [584, 388, 716, 524, 702, 682, 632, 842], [632, 842, 580, 964, 632, 1070, 748, 1214] ]), makeSegments([ [420, -104, 412, 78, 472, 246, 604, 388], [604, 388, 724, 518, 728, 660, 660, 826], [660, 826, 604, 958, 626, 1078, 744, 1214] ]), makeSegments([ [492, -88, 462, 88, 514, 250, 642, 392], [642, 392, 752, 514, 752, 650, 688, 806], [688, 806, 632, 946, 648, 1068, 760, 1204] ]) ];
function makeSegments(rows) { return rows.map(function(row) { return [ { x: row[0], y: row[1] }, { x: row[2], y: row[3] }, { x: row[4], y: row[5] }, { x: row[6], y: row[7] } ]; }); }
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
function resize() { var rect = canvas.getBoundingClientRect(); var nextW = Math.max(1, Math.round(rect.width || window.innerWidth || 1)); var nextH = Math.max(1, Math.round(rect.height || window.innerHeight || 1)); mobile = nextW < 720; dpr = Math.min(window.devicePixelRatio || 1, mobile ? 1.25 : 1.55); var pxW = Math.round(nextW * dpr); var pxH = Math.round(nextH * dpr); if (canvas.width !== pxW || canvas.height !== pxH) { canvas.width = pxW; canvas.height = pxH; } size.w = nextW; size.h = nextH; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function cubicPoint(p0, p1, p2, p3, t) { var mt = 1 - t; var mt2 = mt * mt; var t2 = t * t; return { x: p0.x * mt2 * mt + 3 * p1.x * mt2 * t + 3 * p2.x * mt * t2 + p3.x * t2 * t, y: p0.y * mt2 * mt + 3 * p1.y * mt2 * t + 3 * p2.y * mt * t2 + p3.y * t2 * t }; } function ribbonTransform() { var w = size.w; var h = size.h; var ribbonWidth = mobile ? Math.min(w * 1.22, 820) : Math.min(w * 0.56, controls.ribbonWidth); var ribbonHeight = h + (mobile ? 180 : 250); var sx = ribbonWidth / 760; var sy = ribbonHeight / 1120; var tx = mobile ? w - ribbonWidth + 92 : w - controls.ribbonRight - ribbonWidth; var ty = mobile ? -92 : controls.ribbonTop; return { sx: sx, sy: sy, tx: tx, ty: ty }; } function curvePoint(segments, t, time, phase, amp, offset) { var total = segments.length; var index = Math.min(total - 1, Math.max(0, Math.floor(t * total))); var local = t * total - index; var segment = segments[index]; var point = cubicPoint(segment[0], segment[1], segment[2], segment[3], local); var x = point.x + (offset || 0); var y = point.y; var tx = ribbonTransform(); var depth = Math.sin((t * Math.PI) + phase * 0.07); var living = Math.sin(time * controls.speed + t * 7.2 + phase) * amp * controls.drift * 0.12; var micro = Math.cos(time * controls.speed * 0.72 + t * 15.2 + phase * 0.5) * amp * 0.18; return { x: tx.tx + x * tx.sx + living + micro + depth * (mobile ? 2 : 3.4), y: tx.ty + y * tx.sy + Math.cos(time * 0.28 + t * 5.2 + phase) * (mobile ? 1.4 : 2.4) }; } function makeGradient(band) { var gradient = ctx.createLinearGradient(size.w * 0.62, -size.h * 0.08, size.w * 1.02, size.h * 1.08); band.stops.forEach(function(stop) { gradient.addColorStop(stop[0], stop[1]); }); return gradient; } function tracePath(segments, phase, amp, time, sampleCount, offset) { sampleCount = sampleCount || (mobile ? 46 : 74); for (var i = 0; i <= sampleCount; i += 1) { var t = i / sampleCount; var p = curvePoint(segments, t, time, phase, amp, offset); if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y); } } function strokeRibbonBand(band, time, scale, alpha, blur) { ctx.save(); ctx.globalAlpha = band.alpha * alpha; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.lineWidth = Math.max(1, band.width * scale * (mobile ? 0.66 : 1)); ctx.shadowBlur = blur * (mobile ? 0.55 : 1); ctx.shadowColor = band.glow; ctx.strokeStyle = makeGradient(band); ctx.beginPath(); tracePath(bandSegments[band.name], band.phase, band.amp, time); ctx.stroke(); ctx.restore(); } function drawTexture(time) { ctx.save(); ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.setLineDash([1.2, 5.4]); ctx.lineDashOffset = -time * controls.textureSpeed; ctx.shadowBlur = mobile ? 1.6 : 2.8; ctx.shadowColor = "rgba(255,255,255,.55)"; strandSegments.forEach(function(segments, index) { ctx.globalAlpha = (mobile ? 0.28 : controls.textureOpacity) * (index === 0 || index === strandSegments.length - 1 ? 0.72 : 1); ctx.lineWidth = mobile ? 1.2 : 2.1; ctx.strokeStyle = "rgba(255,255,255,.62)"; ctx.beginPath(); tracePath(segments, index * 0.52, 1.9, time, mobile ? 36 : 64); ctx.stroke(); }); ctx.setLineDash([1, 9]); ctx.lineDashOffset = time * controls.textureSpeed * 0.55; strandSegments.forEach(function(segments, index) { ctx.globalAlpha = mobile ? 0.12 : 0.18; ctx.lineWidth = mobile ? 0.7 : 1; ctx.strokeStyle = "rgba(255,247,226,.42)"; ctx.beginPath(); tracePath(segments, index * 0.39 + 2.1, 1.2, time, mobile ? 32 : 54, (index - 2) * 9); ctx.stroke(); }); ctx.restore(); } function drawEdgeLight(time) { var edges = [ { segments: strandSegments[0], color: "rgba(225,241,255,.22)", width: 1.8, phase: 1.4, offset: -4 }, { segments: strandSegments[4], color: "rgba(255,139,207,.27)", width: 2, phase: 5.8, offset: 4 }, { segments: bandSegments.gold, color: "rgba(255,232,147,.3)", width: 1.45, phase: 3.9, offset: 0 } ]; ctx.save(); ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.shadowBlur = 12; edges.forEach(function(edge) { ctx.globalAlpha = mobile ? 0.2 : 0.34; ctx.lineWidth = edge.width; ctx.strokeStyle = edge.color; ctx.shadowColor = edge.color; ctx.beginPath(); tracePath(edge.segments, edge.phase, 3.8, time, mobile ? 40 : 66, edge.offset || 0); ctx.stroke(); }); ctx.restore(); } function drawAmbientVeil(time) { var w = size.w; var h = size.h; var warm = ctx.createRadialGradient(w * 0.82, h * 0.18, 0, w * 0.82, h * 0.18, Math.max(w, h) * 0.62); warm.addColorStop(0, "rgba(255,180,51,.12)"); warm.addColorStop(0.36, "rgba(247,88,188,.085)"); warm.addColorStop(0.72, "rgba(200,221,255,.055)"); warm.addColorStop(1, "rgba(255,255,255,0)"); ctx.save(); ctx.globalAlpha = mobile ? 0.42 : 0.66; ctx.fillStyle = warm; ctx.fillRect(0, 0, w, h); var cool = ctx.createRadialGradient(w * 0.9, h * (0.56 + Math.sin(time * 0.09) * 0.02), 0, w * 0.9, h * 0.56, Math.max(w, h) * 0.5); cool.addColorStop(0, "rgba(165,132,255,.075)"); cool.addColorStop(0.48, "rgba(206,237,255,.08)"); cool.addColorStop(1, "rgba(255,255,255,0)"); ctx.fillStyle = cool; ctx.fillRect(0, 0, w, h); ctx.restore(); } function renderFrame(now, still) { var time = (now || 0) * 0.001; if (still) time = 7.5; resize(); ctx.clearRect(0, 0, size.w, size.h); drawAmbientVeil(time); ctx.save(); ctx.globalCompositeOperation = "source-over"; bands.forEach(function(band) { strokeRibbonBand(band, time * 0.78, 1.12, controls.glow, mobile ? 16 : 28); }); bands.forEach(function(band) { strokeRibbonBand(band, time, 1, mobile ? 0.72 : 1, mobile ? 4 : 9); }); drawTexture(time); drawEdgeLight(time); ctx.restore(); } function stop() { active = false; if (raf) window.cancelAnimationFrame(raf); raf = 0; } function loop(now) { if (!active) return; renderFrame(now, false); raf = window.requestAnimationFrame(loop); } function start() { stop(); resize(); if (reducedMotion && reducedMotion.matches) { root.classList.add("prelude-reduced-motion"); renderFrame(0, true); return; } root.classList.remove("prelude-reduced-motion"); active = true; raf = window.requestAnimationFrame(loop); } var resizeQueued = false; function requestResize() { if (resizeQueued) return; resizeQueued = true; window.requestAnimationFrame(function() { resizeQueued = false; resize(); renderFrame(performance.now(), reducedMotion && reducedMotion.matches); }); } window.addEventListener("resize", requestResize, { passive: true }); document.addEventListener("visibilitychange", function() { if (document.hidden) stop(); else start(); }); if (reducedMotion && reducedMotion.addEventListener) { reducedMotion.addEventListener("change", start); } start(); })();
/*! * Prelude interaction layer. * * Depth movement is an original DOM/CSS implementation inspired by the motion * language of Codrops / Tympanus "Atmospheric Depth Gallery": * https://tympanus.net/Tutorials/DepthGallery/ * No Codrops production files are hotlinked or copied here; the reference is * used for interaction direction only. Keep this module separate so the effect * can be tuned or disabled without touching the approved Prelude layouts. */ (function() { var root = document.getElementById("prelude-stage-root"); if (!root) return;
var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); var compactViewport = window.matchMedia("(max-width: 900px)"); var scenes = Array.prototype.slice.call(root.querySelectorAll("[data-scene]")); var sceneMetrics = []; var scrollTarget = window.scrollY || 0; var scrollCurrent = scrollTarget; var depthFrame = 0; var depthEnabled = false; var depthLite = false;
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
function measureScenes() { sceneMetrics = scenes.map(function(scene) { return { top: scene.offsetTop, height: Math.max(scene.offsetHeight, window.innerHeight * 0.72) }; }); }
function resetDepthVars() { scenes.forEach(function(scene) { scene.style.removeProperty("--scene-depth-y"); scene.style.removeProperty("--scene-depth-scale"); scene.style.removeProperty("--scene-depth-opacity"); scene.style.removeProperty("--scene-depth-blur"); scene.style.removeProperty("--scene-depth-z"); scene.classList.remove("scene-depth-current"); }); }
function setSceneDepth(scene, delta, focused) { var distance = clamp(Math.abs(delta), 0, 1.45); var eased = Math.pow(distance, 1.12); var y = depthLite ? clamp(delta * -18, -24, 24) : clamp(delta * -42, -72, 72); var scale = depthLite ? 1 : 1 - eased * 0.052; var z = depthLite ? 0 : -eased * 120; var opacity = 1 - clamp((distance - (depthLite ? 0.38 : 0.52)) / 0.86, 0, 1) * (depthLite ? 0.2 : 0.42); var blur = depthLite ? 0 : clamp((distance - 0.68) / 0.72, 0, 1) * 2.4;
scene.style.setProperty("--scene-depth-y", y.toFixed(2) + "px"); scene.style.setProperty("--scene-depth-scale", scale.toFixed(4)); scene.style.setProperty("--scene-depth-opacity", opacity.toFixed(4)); scene.style.setProperty("--scene-depth-blur", blur.toFixed(3) + "px"); scene.style.setProperty("--scene-depth-z", z.toFixed(2) + "px"); scene.classList.toggle("scene-depth-current", focused); }
function renderDepth(immediate) { depthFrame = 0; if (!depthEnabled || !scenes.length) return;
scrollTarget = window.scrollY || window.pageYOffset || 0; if (immediate) { scrollCurrent = scrollTarget; } else { scrollCurrent += (scrollTarget - scrollCurrent) * 0.145; }
var viewportCenter = scrollCurrent + window.innerHeight * 0.5; var bestIndex = 0; var bestDistance = Infinity;
scenes.forEach(function(scene, index) { var metric = sceneMetrics[index]; if (!metric) return; var center = metric.top + metric.height * 0.5; var distance = Math.abs(center - viewportCenter); if (distance < bestDistance) { bestDistance = distance; bestIndex = index; } }); scenes.forEach(function(scene, index) { var metric = sceneMetrics[index]; if (!metric) return; var center = metric.top + metric.height * 0.5; var delta = (center - viewportCenter) / Math.max(window.innerHeight, 1); setSceneDepth(scene, delta, index === bestIndex); }); if (Math.abs(scrollTarget - scrollCurrent) > 0.24) { depthFrame = window.requestAnimationFrame(function() { renderDepth(false); }); } }
function requestDepthRender() { if (!depthEnabled || depthFrame) return; depthFrame = window.requestAnimationFrame(function() { renderDepth(false); }); }
function configureDepth() { depthEnabled = !reducedMotion.matches; depthLite = compactViewport.matches; root.classList.toggle("prelude-depth-active", depthEnabled && !depthLite); root.classList.toggle("prelude-depth-lite", depthEnabled && depthLite); root.classList.toggle("prelude-depth-disabled", !depthEnabled);
if (!depthEnabled) { if (depthFrame) window.cancelAnimationFrame(depthFrame); depthFrame = 0; resetDepthVars(); return; }
measureScenes(); renderDepth(true); }
window.addEventListener("scroll", requestDepthRender, { passive: true }); window.addEventListener("resize", function() { window.requestAnimationFrame(configureDepth); }, { passive: true }); if (reducedMotion.addEventListener) reducedMotion.addEventListener("change", configureDepth); if (compactViewport.addEventListener) compactViewport.addEventListener("change", configureDepth); window.setTimeout(configureDepth, 80); window.setTimeout(configureDepth, 500); window.__preludeInteractions = { refresh: configureDepth, resetDepth: resetDepthVars }; })();

