<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hotel Lobby Lab</title>
    <description>The latest articles on DEV Community by Hotel Lobby Lab (@hotellobby_lab).</description>
    <link>https://dev.to/hotellobby_lab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4218269%2Fed6e737b-d86c-4137-8f86-1e6bda417542.png</url>
      <title>DEV Community: Hotel Lobby Lab</title>
      <link>https://dev.to/hotellobby_lab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hotellobby_lab"/>
    <language>en</language>
    <item>
      <title>hotellobby (LobbyDuo): UX Notes on Preview-Before-Pay AI Booth Videos</title>
      <dc:creator>Hotel Lobby Lab</dc:creator>
      <pubDate>Sat, 31 Oct 2026 04:48:36 +0000</pubDate>
      <link>https://dev.to/hotellobby_lab/hotellobby-lobbyduo-ux-notes-on-preview-before-pay-ai-booth-videos-51ed</link>
      <guid>https://dev.to/hotellobby_lab/hotellobby-lobbyduo-ux-notes-on-preview-before-pay-ai-booth-videos-51ed</guid>
      <description>&lt;p&gt;&lt;em&gt;How I shipped a two-photo → Hotel Lobby booth duo on Cloudflare Workers — optimizing framing preview before Stripe, with Seedance via TryAllAPI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Trend video products usually fail at the same moment: the user pays, then discovers their faces are cropped wrong. For &lt;a href="https://tryhotellobby.app/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=builder" rel="noopener noreferrer"&gt;tryhotellobby.app&lt;/a&gt; (product name &lt;strong&gt;hotellobby / LobbyDuo&lt;/strong&gt;) I optimized the &lt;strong&gt;UX before the charge&lt;/strong&gt;, not another prompt textarea.&lt;/p&gt;

&lt;p&gt;Upload two adult face photos → ~15s orange COLORS-style booth duo (9:16 / 16:9 / 21:9, 480p or 720p) with original/AI booth audio. Independent tool — not affiliated with any artist group. This post is the builder + UX walkthrough. Prices and model ids change; treat live site / console as source of truth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgaqxwp7zk6ot5nuqd1g8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgaqxwp7zk6ot5nuqd1g8.jpg" alt="hotellobby hero — booth duo create experience" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  UX thesis: preview framing, then unlock Create
&lt;/h2&gt;

&lt;p&gt;The homepage copy is blunt: &lt;strong&gt;free scene preview before you pay&lt;/strong&gt; — upload two photos, check framing in the orange booth layout, then unlock Create with a one-time pack.&lt;/p&gt;

&lt;p&gt;That sequence matters more than fancy motion poetry:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Photo 1 → &lt;strong&gt;left&lt;/strong&gt; of the booth duo.
&lt;/li&gt;
&lt;li&gt;Photo 2 → &lt;strong&gt;right&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Local/layout preview of how faces sit in the booth.
&lt;/li&gt;
&lt;li&gt;Only then: Stripe → credits → Seedance render.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users who can see “my face is too small / off-center” before paying convert better and refund less. Free-form “make a hotel video” prompts do the opposite: ambiguity first, charge second, regret third.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgohq6jn69gafdqld6l8y.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgohq6jn69gafdqld6l8y.jpg" alt="Desktop hero screenshot of the LobbyDuo create flow" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live packs verified on-site while writing: &lt;strong&gt;Single $8.88&lt;/strong&gt; (≈ 500 credits / one 720p), &lt;strong&gt;3-pack $16.99&lt;/strong&gt;, plus larger packs (and occasional cheaper 480p messaging). Guest Stripe — no account for a single video. Always re-check the create CTA before you screenshot a price for a friend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why left/right roles beat a single “upload faces” dump
&lt;/h2&gt;

&lt;p&gt;Booth memes are spatial. If the UI does not say who stands where, users invent their own convention and then complain the sample “looks different.” Fixed seats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove “who is who?” support tickets.
&lt;/li&gt;
&lt;li&gt;Stabilize the locked template prompt sent upstream.
&lt;/li&gt;
&lt;li&gt;Make demos honest — the sample matches the seats.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aspect ratio is a first-class control (9:16 default for Reels/TikTok; 16:9 / 21:9 for other cuts). Original/AI booth audio means the download is shareable without hunting a track.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Front / SSR&lt;/td&gt;
&lt;td&gt;TanStack Start (Vite)&lt;/td&gt;
&lt;td&gt;Landing, upload panel, generation status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;Edge deploy, secrets, API routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;Cloudflare D1&lt;/td&gt;
&lt;td&gt;Credits, orders, task rows (table prefixes if DBs are shared)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe Checkout&lt;/td&gt;
&lt;td&gt;One-time packs + optional larger packs; guest checkout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video upstream&lt;/td&gt;
&lt;td&gt;TryAllAPI → Seedance&lt;/td&gt;
&lt;td&gt;Async create + poll&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;GA&lt;/td&gt;
&lt;td&gt;Funnel sanity check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;two faces → booth layout preview
→ check credits (else Stripe)
→ Worker creates Seedance task via TryAllAPI
→ poll until MP4 URL → store / show
→ on failure: refund credits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66311xndelza1y0u2hw6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66311xndelza1y0u2hw6.jpg" alt="LobbyDuo vertical sample / OG poster" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo clips are self-hosted. Do not hotlink someone else’s sample MP4 for marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a video API aggregator
&lt;/h2&gt;

&lt;p&gt;Operational checklist after register / top-up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm &lt;strong&gt;available USD balance&lt;/strong&gt; in the console (not only the checkout success page).
&lt;/li&gt;
&lt;li&gt;Create a token in &lt;strong&gt;pay-as-you-go&lt;/strong&gt; style billing.
&lt;/li&gt;
&lt;li&gt;Enable the &lt;strong&gt;video / Seedance-related channel group&lt;/strong&gt; on that token. Wrong group → “no available channel” even when the model id looks right.
&lt;/li&gt;
&lt;li&gt;Prefer separate keys per product (&lt;code&gt;hotellobby-prod&lt;/code&gt; vs sandbox).
&lt;/li&gt;
&lt;li&gt;Put the key only in Workers Secrets (for example &lt;code&gt;TRYALLAPI_API_KEY&lt;/code&gt;). Never in the frontend bundle or a blog post.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conceptually the call shape looks like this (field names follow the aggregator’s v3 contents API; always re-check current docs):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v3/contents/generations/tasks
Authorization: Bearer &amp;lt;TRYALLAPI_API_KEY&amp;gt;
Content-Type: application/json
Accept-Encoding: identity

GET /api/v3/contents/generations/tasks/{id}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I send &lt;code&gt;Accept-Encoding: identity&lt;/code&gt; from the Worker because some gateways behave badly with transparent decompression. Model ids used in production look like Seedance 2.x family strings — treat console ids as source of truth, not this paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seedance parameters that matter for booth duos
&lt;/h2&gt;

&lt;p&gt;A few details that bit me across this family of sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pass &lt;code&gt;duration&lt;/code&gt; as an explicit integer.&lt;/strong&gt; Some models default to “smart length.” Omitting duration can silently lengthen the clip and the bill.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resolution&lt;/code&gt; and &lt;code&gt;ratio&lt;/code&gt;.&lt;/strong&gt; Cost tracks resolution tier and duration more than portrait vs landscape. Match whatever the create UI offers (often 480p/720p and 9:16 for Shorts).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;generate_audio&lt;/code&gt;.&lt;/strong&gt; When the product ships “finished” downloads, leave audio on; creators who want a trending sound can mute later.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status machine.&lt;/strong&gt; Expect something like &lt;code&gt;queued&lt;/code&gt; → &lt;code&gt;running&lt;/code&gt; → &lt;code&gt;succeeded&lt;/code&gt; / &lt;code&gt;failed&lt;/code&gt;. Success URLs are often &lt;strong&gt;short-lived signed links&lt;/strong&gt; — download/rehost on your side before handing the file to the user.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decent failure.&lt;/strong&gt; On &lt;code&gt;supply_unavailable&lt;/code&gt; or hard failures: refund credits, show a short human message, log the detailed upstream error server-side, optionally fall back to another Seedance tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rough mental model for upstream cost (illustrative only — use your own &lt;code&gt;usage&lt;/code&gt; fields and invoices):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tokens ≈ duration_sec × width × height × fps / 1024
cost   ≈ tokens × price_per_million_tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Price the site &lt;strong&gt;after&lt;/strong&gt; you measure a few real 720p runs with retry margin, Stripe fees, and failed-job refunds — not from a spreadsheet fantasy of 0% failure.&lt;/p&gt;

&lt;p&gt;Booth-specific: map two reference images to left/right seats consistently; same-person clones in both seats look wrong — the UI should say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits, Stripe, and guest checkout
&lt;/h2&gt;

&lt;p&gt;Demo UIs are easy. The line between toy and product is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User hits Create → check credit balance.
&lt;/li&gt;
&lt;li&gt;If short → Stripe Checkout Session (guest allowed).
&lt;/li&gt;
&lt;li&gt;Webhook (plus a return-URL sync check) grants credits.
&lt;/li&gt;
&lt;li&gt;Pre-debit / lock credits, create upstream task, persist &lt;code&gt;task_id&lt;/code&gt; in D1.
&lt;/li&gt;
&lt;li&gt;Poll until done; succeed → show MP4; fail → &lt;strong&gt;refund credits&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two books of account stay separate: &lt;strong&gt;USD balance at TryAllAPI&lt;/strong&gt; (your cost) vs &lt;strong&gt;site credits&lt;/strong&gt; (what you sold). Mixing them in one mental ledger is how you underprice or invent “mysterious” losses.&lt;/p&gt;

&lt;p&gt;When several products share one Stripe account, isolate webhooks by endpoint or by metadata (&lt;code&gt;site&lt;/code&gt; / &lt;code&gt;product&lt;/code&gt;). Callbacks pointed at the wrong apex domain are a classic multi-site footgun.&lt;/p&gt;

&lt;p&gt;Secrets stay in Workers: &lt;code&gt;TRYALLAPI_API_KEY&lt;/code&gt;, &lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;, webhook secret, auth secrets. The browser only ever sees publishable Stripe keys if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy notes (Workers + D1)
&lt;/h2&gt;

&lt;p&gt;Typical loop on this template family:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# wire D1 database_id, production env, custom domain in wrangler&lt;/span&gt;
pnpm cf:deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before calling a deploy “done”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;D1 binding is correct; shared DBs use &lt;strong&gt;table prefixes&lt;/strong&gt;, not a new database per micro-idea (free-tier DB counts are limited).
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AUTH_TRUSTED_ORIGINS&lt;/code&gt; and Stripe return / webhook URLs match the production HTTPS origin.
&lt;/li&gt;
&lt;li&gt;GA id is set for a coarse funnel (land → upload → pay → wait → download).
&lt;/li&gt;
&lt;li&gt;One real end-to-end path on production or a staging Worker: upload → pay → render → download.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Production origin for Checkout / auth trust: &lt;code&gt;https://tryhotellobby.app&lt;/code&gt; (alternate brand hostnames may exist — keep allowlists in sync with the URL users actually pay on).&lt;/p&gt;

&lt;h2&gt;
  
  
  Product rules that are not optional
&lt;/h2&gt;

&lt;p&gt;Face-based generation is not “just another SaaS CRUD app”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Require an explicit &lt;strong&gt;18+ / rights-to-use&lt;/strong&gt; confirmation on upload.
&lt;/li&gt;
&lt;li&gt;Block minors and refuse celebrity / stranger misuse in copy and policy.
&lt;/li&gt;
&lt;li&gt;Expect upstream safety refusals; refund and explain.
&lt;/li&gt;
&lt;li&gt;Keep privacy policy honest about retention; do not train on user media by default.
&lt;/li&gt;
&lt;li&gt;Market as an &lt;strong&gt;independent fan-style tool&lt;/strong&gt; for a trend format — not as an official tie-in unless you actually have that deal.
&lt;/li&gt;
&lt;li&gt;Use your own sample renders in marketing.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those constraints belong in the product, not in a footnote you add after the first dispute email.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the UX taught me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preview-before-pay&lt;/strong&gt; reduces “bad crop” chargebacks more than shaving another dollar off the Single pack.
&lt;/li&gt;
&lt;li&gt;Locked booth layout beats free-form hotel prompts.
&lt;/li&gt;
&lt;li&gt;Guest Single pack (~$8.88) matches “I need one clip tonight.”
&lt;/li&gt;
&lt;li&gt;Credits with automatic refund on failed generation is table stakes.
&lt;/li&gt;
&lt;li&gt;Measure land → upload → pay → wait → download before rewriting marketing adjectives.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Consumer path: &lt;a href="https://tryhotellobby.app/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=builder" rel="noopener noreferrer"&gt;tryhotellobby.app&lt;/a&gt; — watch the sample, upload two authorized adult faces, preview framing, pay once. Builder path: &lt;a href="https://tryallapi.com/register?aff=5A6A&amp;amp;utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=builder" rel="noopener noreferrer"&gt;TryAllAPI&lt;/a&gt;, top up, prove create+poll, then polish the preview UX. &lt;strong&gt;Sell confidence in framing first; sell GPU second.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>ux</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
