# Project export: Otto: The AI Chief Staff for Creators

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Content creators handle many tasks alone, slowing their growth. Otto understands their content, audience, and business, then finds and helps execute the highest-impact opportunities.
- Devpost: https://devpost.com/software/otto-the-ai-chief-staff-for-creators
- GitHub: https://github.com/jawo2/openai_build_AI_COS.git
- Video: https://www.youtube.com/embed/P3wbtjguI18?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Jaime WP (8 commits)

## Devpost submission (written by the team)

### Inspiration

The idea for Otto started with a friend of ours who is a full-time TikTok creator. He's genuinely good at what he does; his content performs, his audience is loyal, but growing the business side of being a creator was a constant guessing game. Which brands should he reach out to? Was he undercharging for sponsored posts? Which of his videos actually signaled an opportunity worth acting on, and which were just noise? TikTok will tell him his engagement rate, his views, his follower growth, but it never tells him what any of that actually means for his business. We started by building him a small pricing calculator, just a quick spreadsheet-style tool to help him figure out what to charge. But the more we used it, the more obvious it became that we'd built the same kind of thing we were trying to move past: another number with no context around it. What he actually needed was something that could analyze his metrics and tell him what to do with them: an outreach email, a rate he could defend, and a content idea grounded in what was already working. Translating raw metrics into something actionable, specific, and ready to execute became the real goal, and that's what turned a pricing calculator into Otto.

### What it does

Otto is an AI Chief of Staff for content creators. Instead of another analytics dashboard telling you your engagement went up, Otto looks at a creator's actual TikTok data and turns it into concrete, ready-to-use actions: A Dashboard that opens with the two or three things that matter most today, plus a running check-in on whether last week's advice actually worked. A Brand Pipeline that identifies real brands worth pitching, drafts the outreach email grounded in the creator's best-performing content, and adjusts the pitch on request. A Content Studio that turns "what's working" into a concrete plan for the next post, tied to both the creator's own breakout content and what's currently trending. A Pricing tool that works like a live calculator: ask "what if a global brand wants a paid ad instead of an organic post" and it recalculates a full rate card on the spot, showing exactly why the number moved.

### How we built it

Otto's frontend is built with Next.js and Tailwind, and the whole experience is powered by a small team of specialized AI agents: a Content Agent, a Growth Agent, and a Business Agent, each analyzing a different slice of the creator's data, coordinated by an AI Manager that synthesizes their findings into a ranked set of priorities using OpenAI's Responses API with Structured Outputs. Real TikTok data is pulled in through an Apify scraper, so every recommendation is grounded in the creator's actual posts, not a mock dataset. On the design side, we used Figma to prototype and iterate on the UI before touching code, which also gave us much clearer, more specific prompts to hand off to Codex instead of trying to describe layouts in words. We used Codex and ChatGPT throughout development to help us design and iterate on the agent prompts, the UI, and the overall architecture.

### Challenges we ran into

The biggest challenge was performance. Running a live scrape and then coordinating multiple AI agents in sequence made the analysis take a long time, which is a real problem when someone is sitting there waiting for their dashboard to load. We spent a lot of time optimizing this: running the agents in parallel instead of one after another, caching results so a repeat analysis doesn't require re-scraping, and designing the loading experience so the wait itself feels like progress rather than dead time.

### Accomplishments we're proud of

What we're most proud of is that Otto's advice doesn't feel generic. Because everything is grounded in a creator's own data (real videos, real engagement numbers, real trends), the recommendations feel tailored to that specific person and are immediately actionable. Nothing Otto suggests requires a creator to figure out what to do next; the email is already drafted, the rate is already calculated, the content idea is already outlined. It also explains the why behind every suggestion, how it actually moves the needle, so each recommendation becomes a bit of a learning opportunity for the creator, not just a task to check off.

### What we learned

We learned a lot about optimization, both technical (how to make a multi-agent pipeline fast enough to feel responsive) and in terms of the advice itself, how to keep every recommendation grounded in real data instead of drifting into generic, one-size-fits-all suggestions. We had to do system prompting and try different versions to create the experience of a trustworthy business partner. This pushed us to be genuinely careful about every number and claim Otto makes, not just optimize for something that looks impressive.

### What's next

for Otto: The AI Chief of Staff for Creators Our next step is getting Otto in front of more creators beyond our friend who inspired it, real people with real accounts, so we can see how well it generalizes across different niches, follower sizes, and platforms. The creator economy is estimated at over $250 billion globally in 2026 and is on track to keep growing at more than 20% a year, with over 200 million creators worldwide, the overwhelming majority of whom are managing their business side entirely on their own, exactly like our friend was. Longer term, we don't think the need Otto solves is unique to creators. Anyone whose income is tied to their online presence: freelancers, consultants, coaches, independent artists, even small business owners building a following around their brand, faces the same problem: plenty of metrics, no translation into what to actually do next. We'd love to grow Otto into that broader space, tracking ongoing client and brand relationships over time, expanding beyond TikTok, and deepening the memory piece so Otto feels less like a tool you check in on and more like a manager who's been with you the whole way.

## README (from the GitHub repository)

# Otto | AI Chief of Staff for Creators

Otto helps creators turn social performance data into concrete next actions: growth priorities, brand outreach, content plans, and pricing guidance.

The current demo supports TikTok and Instagram creator analysis using Apify for scraping and OpenAI for structured agent reasoning.

## What This Project Does

- Scrapes or loads creator profile data for TikTok and Instagram.
- Normalizes recent posts/videos into a shared creator profile format.
- Runs specialized AI agents for content, growth, and business analysis.
- Produces a ranked creator brief with priorities, insights, and recommendations.
- Provides interactive workspaces for:
  - Dashboard priorities
  - Brand pipeline and outreach email drafting
  - Content studio planning
  - Pricing/rate-card guidance

## Setup

1. Install dependencies:

   ```bash
   npm install
   ```

2. Create a local environment file:

   ```bash
   cp .env.example .env.local
   ```

3. Add your local secrets to `.env.local`:

   ```env
   OPENAI_API_KEY=your_openai_key
   APIFY_API_TOKEN=your_apify_token
   DEMO_PASSWORD=choose_a_demo_password
   ```

   `.env.local` is ignored by git. Do not commit real API keys.

4. Start the dev server:

   ```bash
   npm run dev
   ```

5. Open the app:

   ```text
   http://localhost:3000
   ```

Try `_offo` for TikTok or `_offo98` for Instagram.

## API Keys And Demo Data

This repo does not include API keys. That is intentional.

To run live scraping and live AI analysis, each developer needs their own:

- `OPENAI_API_KEY`
- `APIFY_API_TOKEN`

Without keys, the app can still run locally using fallback demo/mock data, but live scraping and AI-generated responses will not work. The app is designed to fall back in this order:

1. Fresh cache, if available
2. Live Apify scrape, if credentials are configured
3. Older cache, if scraping fails
4. Mock demo data from `lib/mockData.ts`

For a public demo, deploy the app yourself and set API keys as server-side environment variables in the hosting provider, for example Vercel Environment Variables. Do not put real keys in GitHub.

## Useful Commands

Run the app locally:

```bash
npm run dev
```

Typecheck:

```bash
npm run typecheck
```

Lint:

```bash
npm run lint
```

Build:

```bash
npm run build
```

Run the agent manager test script:

```bash
npm run test:manager
```

## Data Notes

Live TikTok and Instagram data is fetched through Apify actors when `APIFY_API_TOKEN` is configured.

Normalized creator data includes:

- Platform and handle
- Follower count
- Engagement rate and trend
- Recent posts/videos
- Sponsored-content detection
- Breakout-video detection
- Whether metrics are estimated

TikTok view metrics are treated as public real data. Instagram reach/views are not generally available from public scraping, so Instagram performance metrics are marked as estimated where appropriate.

## OpenAI Usage

The app uses OpenAI structured outputs for predictable agent responses.

OpenAI is used for:

- Content agent: identifies repeatable content patterns and suggests next content moves.
- Growth agent: evaluates audience and performance trends.
- Business agent: turns creator metrics into brand and pricing recommendations.
- Manager agent: synthesizes the specialist outputs into a ranked action plan.
- Email generation: drafts outreach emails using only the recommendation's supporting metrics.
- Interactive workspace chat: refines brand outreach, content plans, and pricing guidance.

Schemas are defined with Zod in `lib/types.ts` and exported to JSON Schema for structured outputs.

## How Codex Accelerated The Workflow

Codex was used as a pair-programming agent throughout the build.

It accelerated the workflow by:

- Scaffolding the Next.js App Router project structure quickly.
- Creating shared Zod schemas and TypeScript types for the data model.
- Implementing Apify scraping, normalization, caching, and mock-data fallback logic.
- Building multi-agent OpenAI workflows with structured outputs.
- Adding API routes for analysis, email generation, brand chat, content chat, and pricing chat.
- Iterating on the UI from rough requirements and Figma screenshots into a working multi-tab product.
- Running local validation after changes with typecheck, lint, build, route checks, and dev-server restarts.

Key product decisions made during the process:

- Keep secrets out of GitHub and use `.env.local` for local credentials.
- Make the demo usable without live credentials through cache/mock fallback.
- Favor structured outputs over free-form AI text for stable product behavior.
- Present Otto as a proactive workspace instead of a passive analytics dashboard.
- Use separate tabs for the main creator workflows: Dashboard, Brand Pipeline, Content Studio, and Pricing.



## Detected evidence (automated analysis)

Indexed codebase: 34 recognized source files, 186 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (41 of 41)

```
.env.example
.eslintrc.json
.gitignore
app/api/analyze/route.ts
app/api/brand-chat/route.ts
app/api/content-chat/route.ts
app/api/generate-email/route.ts
app/api/health/route.ts
app/api/next-actions/route.ts
app/api/otto-welcome/route.ts
app/api/pricing-chat/route.ts
app/dashboard/page.tsx
app/globals.css
app/layout.tsx
app/page.tsx
components/connect-profile-form.tsx
components/dashboard-client.tsx
components/otto-workspace.tsx
components/priority-list.tsx
components/recommendation-panel.tsx
lib/agents/businessAgent.ts
lib/agents/contentAgent.ts
lib/agents/demo-data.ts
lib/agents/growthAgent.ts
lib/agents/index.ts
lib/agents/manager.ts
lib/agents/runAgent.ts
lib/apify.ts
lib/cache.ts
lib/mockData.ts
lib/normalize.ts
lib/openai.ts
lib/types.ts
next-env.d.ts
next.config.mjs
package.json
postcss.config.mjs
README.md
scripts/testManager.ts
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @types/node@20.14.10, @types/react@18.3.3, @types/react-dom@18.3.0, apify-client@^2.23.4, autoprefixer@10.4.19, eslint@8.57.0, eslint-config-next@^14.2.35, next@^14.2.35, openai@^6.48.0, postcss@8.4.39, react@18.3.1, react-dom@18.3.1, tailwindcss@3.4.4, tsx@^4.23.1, typescript@5.5.3, zod@^3.25.76, zod-to-json-schema@^3.25.2

### Recent commits (newest first)

- Polish nav and pricing updates
- Expand README setup and project notes
- Refine creator workspace tabs
- Refine dashboard workspace UI
- Make Otto workspace proactive
- Add local launch instructions
- Add Otto hackathon app
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "otto",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "typecheck": "tsc --noEmit",
    "test:manager": "node --env-file=.env.local --import tsx scripts/testManager.ts"
  },
  "dependencies": {
    "apify-client": "^2.23.4",
    "next": "^14.2.35",
    "openai": "^6.48.0",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "zod": "^3.25.76",
    "zod-to-json-schema": "^3.25.2"
  },
  "devDependencies": {
    "@types/node": "20.14.10",
    "@types/react": "18.3.3",
    "@types/react-dom": "18.3.0",
    "autoprefixer": "10.4.19",
    "eslint": "8.57.0",
    "eslint-config-next": "^14.2.35",
    "postcss": "8.4.39",
    "tailwindcss": "3.4.4",
    "tsx": "^4.23.1",
    "typescript": "5.5.3"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Otto | AI Chief of Staff for Creators",
  description:
    "Otto understands a creator's content, audience, sponsorships, and business, then recommends what to do next."
};

export default function RootLayout({
  children
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

### app/page.tsx

```typescript
import { ConnectProfileForm } from "@/components/connect-profile-form";

export default function LandingPage() {
  return (
    <main className="min-h-screen overflow-hidden bg-paper px-6 py-7 text-ink sm:px-10">
      <section className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-6xl flex-col justify-between">
        <nav className="landing-rise flex items-center justify-between">
          <span className="text-xl font-semibold tracking-tight">Otto</span>
          <a
            className="rounded-full border border-ink/15 px-4 py-2 text-sm font-medium transition hover:border-ink/40"
            href="/dashboard?platform=tiktok&handle=_offo"
          >
            View demo
          </a>
        </nav>

        <div className="grid gap-12 py-14 lg:grid-cols-[1.05fr_0.95fr] lg:items-end">
          <div className="landing-rise max-w-3xl">
            <p className="mb-5 text-sm font-semibold uppercase tracking-[0.18em] text-signal">
              Otto
            </p>
            <h1 className="text-5xl font-semibold leading-[0.95] tracking-tight sm:text-7xl">
              Your AI Chief of Staff for your creator business
            </h1>
            <p className="mt-6 max-w-xl text-lg leading-8 text-ink/68">
              Connect a public profile and Otto turns recent content, audience
              signals, and sponsorship context into the next move.
            </p>
          </div>

          <ConnectProfileForm />
        </div>

        <div className="landing-rise flex flex-wrap items-center gap-x-6 gap-y-2 pb-2 text-sm text-ink/45">
          <span>Live public data</span>
          <span>TikTok first</span>
          <span>Instagram ready</span>
        </div>
      </section>
    </main>
  );
}

```

### app/dashboard/page.tsx

```typescript
import { Suspense } from "react";
import { DashboardClient } from "@/components/dashboard-client";

export default function DashboardPage() {
  return (
    <Suspense fallback={null}>
      <DashboardClient />
    </Suspense>
  );
}

```

### lib/agents/index.ts

```typescript
import type { AnalyzeRequest, AnalyzeResponse } from "@/lib/types";
import { demoAnalysisResult, demoCreatorProfile } from "./demo-data";

export async function analyzeCreator(
  _request: AnalyzeRequest
): Promise<AnalyzeResponse> {
  return {
    creator: demoCreatorProfile,
    analysis: demoAnalysisResult,
    dataSource: demoAnalysisResult.dataSource
  };
}

```

### app/api/health/route.ts

```typescript
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({
    ok: true,
    service: "otto",
    timestamp: new Date().toISOString()
  });
}

```

### app/api/content-chat/route.ts

```typescript
import { NextResponse } from "next/server";
import { runStructuredResponse } from "@/lib/openai";
import {
  CreatorProfileSchema,
  RecommendationSchema
} from "@/lib/types";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const ContentChatRequestSchema = z
  .object({
    currentPlan: z.string().min(1),
    message: z.string().min(1),
    profile: CreatorProfileSchema,
    recommendation: RecommendationSchema.nullable()
  })
  .strict();

const ContentPlanSchema = z
  .object({
    concept: z.string().min(1),
    hook: z.string().min(1),
    script: z.string().min(1),
    caption: z.string().min(1)
  })
  .strict();

const ContentChatResponseSchema = z
  .object({
    reply: z.string().min(1),
    plan: ContentPlanSchema.nullable()
  })
  .strict();

const ContentChatResponseJsonSchema = zodToJsonSchema(ContentChatResponseSchema, {
  name: "ContentChatResponse",
  target: "jsonSchema7"
});

const CONTENT_CHAT_SYSTEM_PROMPT = `You are Otto, an AI Chief of Staff helping a creator execute a content recommendation.

The user is refining a short-form content plan. Answer as Otto, not as a generic chatbot.

Rules:
- Be concise: 1-3 sentences in reply.
- Ground the answer in the profile, recommendation, currentPlan, and recent performance.
- If the user asks for options, a different angle, a shorter script, hooks, captions, or any concrete edit, return an updated plan object.
- If no updated plan is needed, set plan to null.
- Do not invent metrics.
- Keep the tone direct and practical.
- Output only JSON matching the schema.`;

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const parsed = ContentChatRequestSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { error: "Invalid request. Provide profile, recommendation, currentPlan, and message." },
      { status: 400 }
    );
  }

  try {
    const response = await runStructuredResponse({
      name: "ContentChatResponse",
      schema: ContentChatResponseJsonSchema,
      validator: ContentChatResponseSchema,
      instructions: CONTENT_CHAT_SYSTEM_PROMPT,
      input: parsed.data,
      temperature: 0.6
    });

    return NextResponse.json(response);
  } catch (error) {
    return NextResponse.json(
      {
        error:
          error instanceof Error
            ? error.message
            : "Unable to refine this content plan."
      },
      { status: 500 }
    );
  }
}

```

### app/api/brand-chat/route.ts

```typescript
import { NextResponse } from "next/server";
import { runStructuredResponse } from "@/lib/openai";
import {
  CreatorProfileSchema,
  RecommendationSchema
} from "@/lib/types";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const BrandChatRequestSchema = z
  .object({
    currentEmail: z.string().min(1),
    message: z.string().min(1),
    profile: CreatorProfileSchema,
    recommendation: RecommendationSchema
  })
  .strict();

const BrandChatResponseSchema = z
  .object({
    reply: z.string().min(1),
    email: z
      .object({
        subject: z.string().min(1),
        body: z.string().min(1)
      })
      .strict()
      .nullable()
  })
  .strict();

const BrandChatResponseJsonSchema = zodToJsonSchema(BrandChatResponseSchema, {
  name: "BrandChatResponse",
  target: "jsonSchema7"
});

const BRAND_CHAT_SYSTEM_PROMPT = `You are Otto, an AI Chief of Staff helping a creator act on a brand outreach opportunity.

The user is editing or discussing a drafted outreach email. Answer as Otto, not as a generic chatbot.

Rules:
- Be concise: 2-5 sentences unless the user asks for a rewrite.
- Ground the answer in the provided creator profile, recommendation, supportingMetrics, and current email.
- If the user asks for an edit, rewrite, shortening, tone change, different brand, or asks for an email, return the rewritten email in email.body and a short subject in email.subject.
- When email is present, reply should be one short sentence introducing the updated draft. Do not put the full email in reply.
- When email is not needed, set email to null.
- Do not invent metrics, brands, or claims.
- End with a concrete next step when useful.
- Output only JSON matching the schema.`;

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const parsed = BrandChatRequestSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { error: "Invalid request. Provide profile, recommendation, currentEmail, and message." },
      { status: 400 }
    );
  }

  try {
    const response = await runStructuredResponse({
      name: "BrandChatResponse",
      schema: BrandChatResponseJsonSchema,
      validator: BrandChatResponseSchema,
      instructions: BRAND_CHAT_SYSTEM_PROMPT,
      input: parsed.data,
      temperature: 0.5
    });

    return NextResponse.json(response);
  } catch (error) {
    return NextResponse.json(
      {
        error:
          error instanceof Error
            ? error.message
            : "Unable to send this message to Otto."
      },
      { status: 500 }
    );
  }
}

```

### app/api/pricing-chat/route.ts

```typescript
import { NextResponse } from "next/server";
import { runStructuredResponse } from "@/lib/openai";
import {
  CreatorProfileSchema,
  RecommendationSchema
} from "@/lib/types";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const PricingRateRowSchema = z
  .object({
    label: z.string().min(1),
    value: z.string().min(1)
  })
  .strict();

const PricingChatRequestSchema = z
  .object({
    currentRates: z.array(PricingRateRowSchema).min(1),
    message: z.string().min(1),
    profile: CreatorProfileSchema,
    recommendation: RecommendationSchema.nullable()
  })
  .strict();

const PricingChatResponseSchema = z
  .object({
    note: z.string().nullable(),
    rates: z.array(PricingRateRowSchema).nullable(),
    reply: z.string().min(1)
  })
  .strict();

const PricingChatResponseJsonSchema = zodToJsonSchema(PricingChatResponseSchema, {
  name: "PricingChatResponse",
  target: "jsonSchema7"
});

const PRICING_CHAT_SYSTEM_PROMPT = `You are Otto, an AI Chief of Staff helping a creator price sponsored work.

The user is discussing a creator rate card. Answer as Otto, not as a generic chatbot.

Rules:
- Be concise: 1-3 sentences in reply.
- Ground the answer in the creator profile, recommendation, currentRates, and supportingMetrics.
- If the user asks about a specific brand, paid ad, usage rights, whitelisting, exclusivity, bundle, rush work, or any concrete pricing adjustment, return updated rates.
- Rate values must be formatted as ranges without the currency prefix, for example "520-650". The UI adds "S/".
- Preserve the same row labels unless the user asks for a new package.
- If no updated rate card is needed, set rates to null and note to null.
- Do not invent metrics, audience facts, or benchmark claims.
- Output only JSON matching the schema.`;

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const parsed = PricingChatRequestSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { error: "Invalid request. Provide profile, recommendation, currentRates, and message." },
      { status: 400 }
    );
  }

  try {
    const response = await runStructuredResponse({
      name: "PricingChatResponse",
      schema: PricingChatResponseJsonSchema,
      validator: PricingChatResponseSchema,
      instructions: PRICING_CHAT_SYSTEM_PROMPT,
      input: parsed.data,
      temperature: 0.5
    });

    return NextResponse.json(response);
  } catch (error) {
    return NextResponse.json(
      {
        error:
          error instanceof Error
            ? error.message
            : "Unable to update pricing."
      },
      { status: 500 }
    );
  }
}

```

### app/api/otto-welcome/route.ts

```typescript
import { CreatorProfileSchema, RecommendationSchema } from "@/lib/types";
import { DEFAULT_MODEL, openai } from "@/lib/openai";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const WELCOME_SYSTEM_PROMPT = `You are Otto, a proactive AI Chief of Staff for a creator business.

Write the first message Otto sends after analysis completes.

Structure exactly:
1. Greeting. Use "Good morning, Jaime".
2. Main insight. Explain the single highest-impact opportunity discovered.
3. Supporting reasoning. Reference exact supporting metrics and reasoning from the recommendation.
4. Proactive work. Explain what Otto has already prepared.
5. End with exactly: "What would you like me to do next?"

Rules:
- Do not sound like a generic chatbot.
- Do not say you are waiting for instructions.
- Only mention exact metric values from allowedMetricValues.
- Do not calculate or introduce new percentages, ratios, averages, multipliers, deltas, or comparisons.
- Do not mention metrics from profile unless that exact value is also in allowedMetricValues.
- Keep it concise, under 140 words.
- Mention prepared assets generally, not as a long list.
- Plain text only.`;

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const parsedProfile = CreatorProfileSchema.safeParse(body?.profile);
  const parsedRecommendation = RecommendationSchema.safeParse(body?.recommendation);

  if (!parsedProfile.success || !parsedRecommendation.success) {
    return new Response("Invalid request. Provide { profile, recommendation }.", {
      status: 400
    });
  }

  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      try {
        const responseStream = await openai.responses.create({
          model: DEFAULT_MODEL,
          instructions: WELCOME_SYSTEM_PROMPT,
          input: JSON.stringify({
            allowedMetricValues: parsedRecommendation.data.supportingMetrics.map(
              (metric) => metric.value
            ),
            profile: {
              contentCategories: parsedProfile.data.contentCategories,
              handle: parsedProfile.data.handle,
              platform: parsedProfile.data.platform
            },
            recommendation: parsedRecommendation.data
          }),
          temperature: 0.5,
          store: false,
          stream: true
        });

        for await (const event of responseStream) {
          if (event.type === "response.output_text.delta") {
            controller.enqueue(
              encoder.encode(`event: delta\ndata: ${JSON.stringify({ delta: event.delta })}\n\n`)
            );
          }
        }

        controller.enqueue(encoder.encode(`event: done\ndata: {}\n\n`));
      } catch (error) {
        controller.enqueue(
          encoder.encode(
            `event: error\ndata: ${JSON.stringify({
              message:
                error instanceof Error
                  ? error.message
                  : "Unable to stream Otto welcome."
            })}\n\n`
          )
        );
      } finally {
        controller.close();
      }
    }
  });

  return new Response(stream, {
    headers: {
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
      "Content-Type": "text/event-stream"
    }
  });
}

```

[23 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]