diff --git a/.env.example b/.env.example
index 1012a5c0a..eaf4fd17e 100644
--- a/.env.example
+++ b/.env.example
@@ -3,6 +3,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SUPABASE_URL=
OPENAI_API_KEY=
NEXT_PUBLIC_PRIVY_APP_ID=
+# Optional: local api or prod (see docs/PRE_TRIAL_LOCAL.md). Default local = test-recoup-api.
+NEXT_PUBLIC_RECOUP_API_URL=http://localhost:3001
PERPLEXITY_MCP_SERVER=
XAI_API_KEY=
TWILIO_ACCOUNT_SID=your_account_sid
diff --git a/components/Header/Header.tsx b/components/Header/Header.tsx
index eda5084f5..219fd5528 100644
--- a/components/Header/Header.tsx
+++ b/components/Header/Header.tsx
@@ -1,7 +1,7 @@
"use client";
import { MenuIcon, PlusCircle } from "lucide-react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import SideMenu from "../SideMenu";
import { useArtistProvider } from "@/providers/ArtistProvider";
import ImageWithFallback from "../ImageWithFallback";
@@ -10,8 +10,10 @@ import SideArtists from "../SideArtists";
import type { ArtistRecord } from "@/types/Artist";
const Header = () => {
+ const [mounted, setMounted] = useState(false);
const [isOpenMobileMenu, setIsOpenMobileMenu] = useState(false);
const [isOpenSideArtists, setIsOpenSideArtists] = useState(false);
+ useEffect(() => setMounted(true), []);
const {
selectedArtist,
toggleSettingModal,
@@ -20,6 +22,7 @@ const Header = () => {
sorted,
} = useArtistProvider();
const isMobile = useIsMobile();
+ const showMobileChrome = mounted && isMobile;
const isArtistSelected = selectedArtist !== null;
const handleClickPfp = () => {
@@ -38,7 +41,8 @@ const Header = () => {
return (
<>
-
+
+ {showMobileChrome ? (
{
>
+ ) : null}
- {/* Show Add/Select Artist button when on mobile, logged in, and no artist selected */}
- {isMobile && !isArtistSelected && (
+ {/* After mount + mobile only — avoids hydration mismatch (useMediaQuery false on server) */}
+ {showMobileChrome && !isArtistSelected && (
{
)}
{/* Show artist profile when artist is selected */}
- {selectedArtist && isMobile && (
+ {selectedArtist && showMobileChrome && (
{
)}
- {isMobile && (
+ {showMobileChrome && (
<>
chat
+cd chat
+pnpm install
+# If pnpm complains about TTY: $env:CI='true'; pnpm install
+```
+
+## 2. Env (minimum to boot)
+
+Copy `.env.example` → `.env.local`.
+
+**Required for the app to start**
+
+- `NEXT_PUBLIC_PRIVY_APP_ID` — create a free app at [Privy](https://dashboard.privy.io), add **http://localhost:3000** to allowed URLs.
+
+**Required for account / DB features**
+
+- `NEXT_PUBLIC_SUPABASE_URL`
+- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
+- Often `SUPABASE_SERVICE_ROLE_KEY` (server routes)
+
+Pre-trial devs: doc says you may **obtain your own** env or ask Sweetman for **specific** keys — not a generic “send everything” request.
+
+## 3. Run
+
+```bash
+pnpm dev
+```
+
+Open **http://localhost:3000** → screenshot (that’s the pre-trial check).
+
+## 4. Recoup API (401 locally)
+
+Local/preview uses **`https://test-recoup-api.vercel.app`** by default. If calls return **401**, the test deployment may not accept your Privy JWT yet. Options:
+
+| Option | What to do |
+|--------|------------|
+| A | Ask Sweetman to align **test** API with your Privy app (proper fix for “all local”). |
+| B | Temporarily set `NEXT_PUBLIC_RECOUP_API_URL=https://recoup-api.vercel.app` only if you’re allowed to hit prod from localhost. |
+| C | Run **api** locally — see **`api/docs/RUN_LOCAL.md`** — then `NEXT_PUBLIC_RECOUP_API_URL=http://localhost:3001` in chat. |
+
+**Pre-trial goal** is usually: app runs + login works + screenshot — not necessarily every API green on day one.
+
+## 5. “Run everything locally”
+
+Not required for pre-trial. Full stack is roughly:
+
+| Repo | Role |
+|------|------|
+| **chat** | UI (this repo) |
+| **api** | Recoup HTTP API + sandbox setup route |
+| **tasks** | Trigger.dev jobs (needs Trigger credentials) |
+| **mono** | Cloned inside sandboxes by tasks — not run as a separate local server for chat |
+
+For daily work, most devs use **deployed** test/prod API + local chat.
+
+## 6. Windows gotchas
+
+- **EBUSY on `.next`**: stop all `pnpm dev`, delete `.next`, one dev server only; exclude `.next` from OneDrive/antivirus if needed.
+- **Node**: LTS **20** or **22** if Next acts up on Node 24.
diff --git a/hooks/useSandboxSetupOnLogin.tsx b/hooks/useSandboxSetupOnLogin.tsx
new file mode 100644
index 000000000..45a65d86e
--- /dev/null
+++ b/hooks/useSandboxSetupOnLogin.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { useEffect } from "react";
+import { usePrivy } from "@privy-io/react-auth";
+import { useAccessToken } from "@/hooks/useAccessToken";
+import { setupSandbox } from "@/lib/sandboxes/setupSandbox";
+
+const SESSION_KEY = "sandboxSetupDone";
+
+export function useSandboxSetupOnLogin() {
+ const { authenticated } = usePrivy();
+ const accessToken = useAccessToken();
+
+ useEffect(() => {
+ if (!authenticated || !accessToken) {
+ return;
+ }
+
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ if (window.sessionStorage.getItem(SESSION_KEY) === "1") {
+ return;
+ }
+
+ setupSandbox(accessToken)
+ .then(() => {
+ window.sessionStorage.setItem(SESSION_KEY, "1");
+ })
+ .catch((error) => {
+ // We deliberately log and continue so login UX is not blocked
+ console.error("Failed to setup sandbox on login:", error);
+ });
+ }, [authenticated, accessToken]);
+}
+
diff --git a/lib/consts.ts b/lib/consts.ts
index ccd36201b..8f523ad41 100644
--- a/lib/consts.ts
+++ b/lib/consts.ts
@@ -3,7 +3,7 @@ import { Address } from "viem";
export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
export const NEW_API_BASE_URL = IS_PROD
? "https://recoup-api.vercel.app"
- : "https://test-recoup-api.vercel.app";
+ : "http://localhost:3001";
export const IN_PROCESS_PROTOCOL_ADDRESS = IS_PROD
? ("0x540C18B7f99b3b599c6FeB99964498931c211858" as Address)
: ("0x6832A997D8616707C7b68721D6E9332E77da7F6C" as Address);
diff --git a/lib/sandboxes/setupSandbox.ts b/lib/sandboxes/setupSandbox.ts
new file mode 100644
index 000000000..eef27a623
--- /dev/null
+++ b/lib/sandboxes/setupSandbox.ts
@@ -0,0 +1,28 @@
+import { NEW_API_BASE_URL } from "@/lib/consts";
+
+type SetupSandboxResponse =
+ | {
+ status: "success";
+ runId: string;
+ }
+ | {
+ status: "error";
+ error: string;
+ };
+
+export async function setupSandbox(accessToken: string): Promise {
+ const response = await fetch(`${NEW_API_BASE_URL}/api/sandboxes/setup`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to setup sandbox: ${response.status} ${response.statusText}`);
+ }
+
+ return (await response.json()) as SetupSandboxResponse;
+}
+
diff --git a/next.config.mjs b/next.config.mjs
index c5387a5b5..53618720b 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -21,42 +21,55 @@ const nextConfig = {
],
},
images: {
- domains: [
- "i.imgur.com",
- "ipfs.decentralized-content.com",
- "pbs.twimg.com", // Twitter profile images
- "abs.twimg.com", // Twitter media
- "cdn.discordapp.com", // Discord
- "scontent.xx.fbcdn.net", // Facebook
- "scontent.cdninstagram.com", // Instagram
- "instagram.fyvr4-1.fna.fbcdn.net", // Instagram
- "platform-lookaside.fbsbx.com", // Facebook
- "static-cdn.jtvnw.net", // Twitch
- "yt3.ggpht.com", // YouTube
- "i.ytimg.com", // YouTube
- "avatars.githubusercontent.com", // GitHub
- "example.com", // Example domain from our mock data
- "arweave.net", // Arweave
- "storage.googleapis.com", // Fal AI image hosting (backup)
- ],
remotePatterns: [
{
- protocol: 'https',
- hostname: '*.fal.media',
- port: '',
- pathname: '/**',
+ protocol: "https",
+ hostname: "*.fal.media",
+ pathname: "/**",
+ },
+ {
+ protocol: "https",
+ hostname: "ipfs.decentralized-content.com",
+ pathname: "/**",
+ },
+ {
+ protocol: "https",
+ hostname: "arweave.net",
+ pathname: "/**",
+ },
+ { protocol: "https", hostname: "i.imgur.com", pathname: "/**" },
+ { protocol: "https", hostname: "pbs.twimg.com", pathname: "/**" },
+ { protocol: "https", hostname: "abs.twimg.com", pathname: "/**" },
+ { protocol: "https", hostname: "cdn.discordapp.com", pathname: "/**" },
+ { protocol: "https", hostname: "scontent.xx.fbcdn.net", pathname: "/**" },
+ {
+ protocol: "https",
+ hostname: "scontent.cdninstagram.com",
+ pathname: "/**",
+ },
+ {
+ protocol: "https",
+ hostname: "instagram.fyvr4-1.fna.fbcdn.net",
+ pathname: "/**",
+ },
+ {
+ protocol: "https",
+ hostname: "platform-lookaside.fbsbx.com",
+ pathname: "/**",
},
+ { protocol: "https", hostname: "static-cdn.jtvnw.net", pathname: "/**" },
+ { protocol: "https", hostname: "yt3.ggpht.com", pathname: "/**" },
+ { protocol: "https", hostname: "i.ytimg.com", pathname: "/**" },
{
- protocol: 'https',
- hostname: 'ipfs.decentralized-content.com',
- port: '',
- pathname: '/**',
+ protocol: "https",
+ hostname: "avatars.githubusercontent.com",
+ pathname: "/**",
},
+ { protocol: "https", hostname: "example.com", pathname: "/**" },
{
- protocol: 'https',
- hostname: 'arweave.net',
- port: '',
- pathname: '/**',
+ protocol: "https",
+ hostname: "storage.googleapis.com",
+ pathname: "/**",
},
],
},
diff --git a/providers/Providers.tsx b/providers/Providers.tsx
index daf9e895a..80a261485 100644
--- a/providers/Providers.tsx
+++ b/providers/Providers.tsx
@@ -13,6 +13,7 @@ import WagmiProvider from "./WagmiProvider";
import { MiniAppProvider } from "./MiniAppProvider";
import { ThemeProvider } from "./ThemeProvider";
import { OrganizationProvider } from "./OrganizationProvider";
+import SandboxSetupOnLogin from "./SandboxSetupOnLogin";
const queryClient = new QueryClient();
@@ -30,6 +31,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => (
+
diff --git a/providers/SandboxSetupOnLogin.tsx b/providers/SandboxSetupOnLogin.tsx
new file mode 100644
index 000000000..655ecfd31
--- /dev/null
+++ b/providers/SandboxSetupOnLogin.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import { useSandboxSetupOnLogin } from "@/hooks/useSandboxSetupOnLogin";
+
+const SandboxSetupOnLogin = () => {
+ useSandboxSetupOnLogin();
+ return null;
+};
+
+export default SandboxSetupOnLogin;
+