---
name: oeave-project
description: 'Build or edit an Oeave project through the Agent HTTP API. Creates fibers, threads, components, style, uploads assets, triggers depth maps / embeddings, and publishes. Use when the user asks you to "build", "flesh out", "fill in", "populate", "publish" or "edit" an Oeave project.'
---

# Oeave Project Builder

You're building content for an Oeave project using the Agent HTTP API. This document tells you the workflow patterns that work reliably. The base URL and endpoint catalog are authoritative — when in doubt, call `/docs` and follow what it says.

---

## 0. Credentials

Before any API call, make sure you have the user's API key in the environment:

```
OEAVE_API_KEY=oea_live_<prefix>_<secret>
```

**If you don't have a key, stop and direct the user to generate one.** Tell them, in plain terms:

> "I need an Oeave API key to build in your project. Go to [https://www.oeave.com/settings/](https://www.oeave.com/settings/) and scroll to the **API keys** section. Click **New API key**, name it (e.g. _Claude Code_), leave the scopes on **Full (no delete)** unless you want to restrict me, and click **Create key**. The secret is shown once — copy it and paste it back here (or set it as `OEAVE_API_KEY` in your env)."

Never embed the key in code files or commit it to git — use it as an env var only. If the key leaks, the user should revoke it from the same settings page.

The API base URL is hardcoded:

```
https://api.oeave.com/agent/v1
```

All paths below are relative to that.

---

## 1. ALWAYS fetch `/docs` first

The fiber kinds, component kinds, scopes, endpoints, quotas, and error codes can evolve. Start every session with:

```bash
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  https://api.oeave.com/agent/v1/docs | jq
```

Read `conventions`, `endpoints`, `fiberKinds`, `componentKinds`, and `style`. Treat it as the source of truth — if anything in this document contradicts `/docs`, follow `/docs`.

Then call `/me` to confirm the key works, see your tier, scopes, current monthly usage, and limits:

```bash
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  https://api.oeave.com/agent/v1/me | jq
```

- `401` → the key is invalid/revoked/expired.
- `403 insufficient_scope` on a later call → ask the user to regenerate a key with the missing scope (the error includes `error.details.requiredScope`).

---

## 2. Pick the project

One key can see multiple projects. List them:

```bash
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  https://api.oeave.com/agent/v1/projects | jq
```

Ask the user which one to work on, or accept a slug/name in the invocation. Every subsequent path needs the project id as `:pid`.

---

## 3. Understand what's already there

Before creating anything, survey:

```bash
PID=<project_id>

# Existing fibers (content atoms)
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  "https://api.oeave.com/agent/v1/projects/$PID/fibers" \
  | jq '.data.items[] | {id, kind, title: .data.title, order}'

# Existing threads
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  "https://api.oeave.com/agent/v1/projects/$PID/threads" \
  | jq '.data.items[] | {id, title, funnelStage}'

# Existing assets
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  "https://api.oeave.com/agent/v1/projects/$PID/assets" \
  | jq '.data.items[] | {id, kind, fileName}'

# Style
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  "https://api.oeave.com/agent/v1/projects/$PID/style" \
  | jq '.data.desktop | {presentationMode, colors, headingFont: .headingFont.family}'
```

**Do not duplicate.** If a fiber on the same topic exists, prefer PATCH over POST. Before creating a fiber on a non-trivial topic, run a vector search against what's already stored:

```bash
curl -s -G -H "Authorization: Bearer $OEAVE_API_KEY" \
  --data-urlencode "q=battery life" \
  "https://api.oeave.com/agent/v1/projects/$PID/search/fibers" | jq '.data.items[] | {id, kind, score, title: .data.title}'
```

Items with a high score (> ~0.75) likely cover the topic — PATCH them instead of creating a sibling.

---

## 3b. Ask where to source content from

Before asking the user *what* to build, ask *where you should pull from*. Fibers and threads need real source material — without it you'll either fabricate or nag the user for every field.

Prompt the user for one or more of:

- **A website URL** — marketing site, docs, landing page. You can scrape copy directly, and use `POST /projects/:pid/style/import-from-url` to pull colors/fonts/logo.
- **A code repo or local path** — README, docs folder, marketing pages in the repo. Read files via your own filesystem tools and mine them for features, setup steps, specs.
- **Uploaded files** — PDFs, decks, spec sheets, images, 3D models, videos the user can point you to on disk.
- **Pasted notes** — raw text the user drops into the chat (positioning, audience, taglines).
- **Existing fibers/threads** — if the survey in §3 already shows meaningful content, you may not need new sources; offer to augment what's there.

Once you know the sources, propose a direction (survey-first, specific gap, full funnel, style import, act on suggestions, publish) grounded in what's actually available. If the user picks a direction without naming a source, ask for one before creating fibers.

---

## 4. Mental model

- **Project** has one **Product** (typically 1:1). The product holds a `threads[]` array.
- **Fibers** are independent content atoms (image, featureCard, testimonial, …). They're stored per-project and referenced by threads.
- **Threads** are ordered sequences of **Components**. Some components reference fibers via `fiberIds`; others carry inline content.
- **Style** is a `{ desktop, mobile }` theme.
- **Embeddings** power vector search and are rebuilt automatically whenever a fiber/thread is written. The fiber's content is fingerprinted on each successful embed; identical content + question config short-circuits subsequent reembeds.

---

## 4b. Coverage principles

**Always populate a `productProfile` fiber.** It's the anchor for the whole project — name, tagline, category, audience, key attributes. If one doesn't exist, create it early from the content sources (§3b). If it exists but is thin, augment it via PATCH before building other fibers. Never leave it empty.

**Be comprehensive — more fibers, better experience.** The renderer, search, suggestions, and embeddings all improve as fiber coverage grows. Don't stop at the minimum viable set. After the obvious fibers (productProfile, brandStory, a few featureCards), keep going: testimonials, faqs, howItWorks, pricing, pressMention, caseStudy, comparison, useCase, problemStatement, guarantee, targetAudience, coreValues, designPhilosophy, behindTheScenes, timeline — anything the sources support. Prefer adding one more well-sourced fiber over stopping early.

**Find and upload strong brand imagery.** Visuals carry the experience. Hunt through the provided sources for:

- **Hero/lifestyle shots, product photos, mood imagery** — upload as assets and reference from `image`, `featureCard`, `testimonial`, `caseStudy`, `productProfile`, `brandStory` fibers. Use `variants: ["depth"]` on hero/mood shots so the 3D depth effect works.
- **Logos and brand graphics** — populate `style.desktop.brandGraphic` (and `mobile.brandGraphic`) with the brand's logo/wordmark. Prefer transparent PNG or SVG; upload as an asset and reference `{ assetId }`.
- **If the source is a website**, scrape its `<img>` tags, OG images, and favicons; if it's a repo, check `public/`, `assets/`, `static/`, and README images. If nothing usable exists, tell the user and ask them to point you at image files or a press kit rather than shipping a text-only project.

---

## 5. Workflow patterns

### 5a. Uploading a media asset

Any `{ assetId }` shorthand in a fiber/component/style body requires an asset row. To create one:

```bash
# 1) Compute sha256 of the file
SHA=$(shasum -a 256 bottle.jpg | awk '{print $1}')

# 2) Request an upload URL
RESP=$(curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  -H "Content-Type: application/json" \
  -X POST "https://api.oeave.com/agent/v1/projects/$PID/assets/uploads" \
  -d "{\"fileName\":\"bottle.jpg\",\"contentType\":\"image/jpeg\",\"sha256\":\"$SHA\"}")

# If reused==true, grab the existing assetId and skip the upload.
# Otherwise:
UPLOAD_URL=$(echo "$RESP" | jq -r .data.uploadUrl)
R2_KEY=$(echo "$RESP" | jq -r .data.r2Key)
PUBLIC_URL=$(echo "$RESP" | jq -r .data.publicUrl)

# 3) PUT the file to uploadUrl (expires in 10 min)
curl -X PUT -T bottle.jpg -H "Content-Type: image/jpeg" "$UPLOAD_URL"

# 4) Probe dimensions locally, then finalize with width/height
WIDTH=$(sips -g pixelWidth bottle.jpg | awk '/pixelWidth/{print $2}')   # or: identify -format "%w" bottle.jpg
HEIGHT=$(sips -g pixelHeight bottle.jpg | awk '/pixelHeight/{print $2}') # or: identify -format "%h" bottle.jpg

curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  -H "Content-Type: application/json" \
  -X POST "https://api.oeave.com/agent/v1/projects/$PID/assets/finalize" \
  -d "{\"r2Key\":\"$R2_KEY\",\"publicUrl\":\"$PUBLIC_URL\",\"sha256\":\"$SHA\",\"fileName\":\"bottle.jpg\",\"contentType\":\"image/jpeg\",\"fileSize\":$(stat -f%z bottle.jpg),\"width\":$WIDTH,\"height\":$HEIGHT,\"variants\":[\"depth\"]}"
```

The finalize response is `202` and includes `pending[].taskId` — poll at `/tasks/:id`. Use `variants: ["depth"]` only for images where the depth map matters (mood shots, product heros) — it's a metered Replicate call.

**Always send `width` and `height` at finalize for images.** The schema marks them optional, but any asset referenced from `style.desktop.brandGraphic.images` (or any other srcSet-consuming location) is validated against a schema that requires them — you'll hit `Object is missing the required field 'height'` on the downstream PATCH, not on finalize. Probe dimensions locally before finalizing. For SVGs, use the `viewBox` dims or a sensible nominal (e.g. 1000×1000).

**Uploads are deduplicated by sha256, and you cannot fix metadata after the fact.** `POST /assets/uploads` with a previously-seen sha256 returns `{reused: true, assetId}` and skips the PUT — re-finalizing with new `width`/`height` is a no-op. The asset row's metadata is frozen at first finalize.

**The `agent:full` scope bundle does NOT include `assets:delete`** (or `threads:delete` / `fibers:delete`). If you finalize an asset with wrong metadata, there's no recovery via the API — your only options are to re-encode the file to change the sha256 (e.g. `ffmpeg -i in.webp -c:v libwebp -q:v 85 out.webp`, `sips -s format png in.png --out out.png`, or appending a comment to an SVG) and upload the new variant, or ask the user to delete it through the UI. Get finalize right the first time.

### 5b. Creating a fiber

Refer to `GET /docs/fiber-kinds` for exact shapes. Shape of a fiber create:

```bash
curl -s -H "Authorization: Bearer $OEAVE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -X POST "https://api.oeave.com/agent/v1/projects/$PID/fibers" \
  -d '{
    "kind": "featureCard",
    "data": {
      "title": "Dual-layer insulation",
      "text": "24h cold, 12h hot — tested in the Alps.",
      "icon": { "assetId": "as_abc" }
    }
  }'
```

Response is `202`. `data` is the stored fiber (fully hydrated). `pending[]` lists the embedding task. You can immediately reference `data.id` in thread components — you don't have to wait for embedding to finish.

**Use `kind` names (strings), not numbers** — e.g. `"featureCard"`, `"image"`, `"threeDModel"`. See `/docs/fiber-kinds` for the canonical list.

### 5c. Updating a fiber

Merge (default) — only fields you send are replaced:

```bash
curl -X PATCH "https://api.oeave.com/agent/v1/projects/$PID/fibers/$FID" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -H "If-Match: 1713000000000" \
  -d '{"data": {"text": "Improved wording."}}'
```

- `If-Match` is the version from the prior GET (equals `updatedAt`). 409 if someone else wrote first.
- `"op": "replace"` to overwrite `data` wholesale.
- `"skipReembed": true` to avoid burning embedding tokens for cosmetic changes.
- `"embedding": { "mode": "auto"|"custom"|"none", "questions": ["…"], "force": false }` — control how the embedding is generated. See §5c-bis.

### 5c-bis. Controlling fiber embeddings

Every fiber that gets created or has its `data` updated triggers a background `embed_fiber_plan` task that splits the fiber's text into chunks, generates HyDE-style questions per chunk, and stores 1536-dim vectors in `fiberEmbeddings`. You can shape that pipeline three ways: pre-supplied questions, a hash-skip on unchanged content, and a force flag.

**The `embedding` payload** (accepted on `POST /fibers`, `PATCH /fibers/:fid`, and `POST /fibers/:fid/reembed`):

```jsonc
{
  "embedding": {
    "mode": "auto",                    // "auto" (default) | "custom" | "none"
    "questions": ["How long does the battery last?", "..."],
    "force": false                     // default false
  }
}
```

`POST /fibers/:fid/reembed` also accepts the same fields at the top level (no `embedding` envelope) since the request has no other body — pick whichever shape you prefer.

**`mode` semantics:**

| `mode` | Behavior |
|---|---|
| `"auto"` | LLM authors 3–5 questions per chunk (default — matches legacy behavior). |
| `"custom"` | Embed your `questions` array verbatim alongside every chunk. Required when you want the search index to retrieve the fiber against specific phrasings (e.g. internal taxonomy, FAQ-style retrieval). Up to 20 questions; empty/whitespace strings are dropped. Sending `questions` without `mode` is treated as `"custom"`. |
| `"none"` | Embed chunk text only, no questions. Use for fibers where the canonical text is already the optimal retrieval surface (raw transcripts, structured specs). |

**Hash-skip on unchanged content.** Each successful embed stores a content fingerprint on the fiber. The next reembed call computes the same fingerprint and short-circuits if it matches — the response is still 202, but the task finishes immediately with `result: { skipped: "unchanged" }`. This makes idempotent "reembed if needed" loops cheap. The fingerprint covers both the chunk text and the question config (mode + custom questions), so changing only the question mode still triggers a real rebuild.

The PATCH path automatically clears the fingerprint when `data` changes, so a fiber edit always re-embeds (unless you also pass `skipReembed: true`).

**`force: true`** bypasses the hash skip. Use it when you've changed something that doesn't bump the fingerprint — e.g. an embedding model rollout, a chunking-rule fix, or you just want a clean rebuild.

```bash
# Reembed only if content changed (no-op if cached)
curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/fibers/$FID/reembed" \
  -H "Authorization: Bearer $OEAVE_API_KEY"

# Reembed with custom retrieval questions
curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/fibers/$FID/reembed" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"mode":"custom","questions":["How fast does this charge?","Is it waterproof?"]}'

# Force a rebuild even if cached
curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/fibers/$FID/reembed" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"force":true}'

# On create — pre-supply the retrieval questions
curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/fibers" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "kind": "faq",
    "data": {"question":"Is it dishwasher safe?","answer":"Top rack only."},
    "embedding": { "mode": "custom", "questions": ["Can I put it in the dishwasher?", "Will it warp in a dishwasher?"] }
  }'
```

Poll the returned `taskId` to see whether the run actually re-embedded — `result.skipped === "unchanged"` means the cache hit, `result.indexed` is the chunk count when it ran for real.

**What makes a good `questions` array.** The questions are concatenated with the chunk text and embedded together — they pull the fiber into retrieval whenever a user (or another agent) phrases their search the same way. Treat them as the index entries you'd file the fiber under, not as comprehension prompts.

Aim for:

- **Real user phrasings, not your taxonomy.** Mirror how a buyer/operator/dev would actually type the question in a chat box or a search bar. "How long does the battery last?" beats "What is the battery duration specification?".
- **Cover the variants.** Include synonyms, abbreviations, and the negative form. For a waterproofing fiber: `"Is it waterproof?" / "Can I take it in the shower?" / "What's the IP rating?" / "Will it survive rain?"`.
- **Anchor on the noun the user knows.** Brand name, product name, or feature name as it's actually spoken. "Does the Stanley Quencher fit a cup holder?" out-retrieves "Does this fit a cup holder?".
- **Mix breadth and specificity.** 2–3 broad-topic questions plus 1–2 sharp specifics; both shapes show up in real traffic.
- **One thought per question.** "Is it fast and durable?" doesn't retrieve well — split into two.
- **3–5 is the sweet spot.** Up to 20 is allowed, but past ~6 you mostly add noise. Quality over quantity.

Avoid:

- **Restating the title.** "What is X?" is what the chunk text already says — you're spending tokens on a duplicate.
- **Yes/no without the topic.** "Does it do that?" matches everything; include the noun.
- **Marketing voice.** "Why is our insulation industry-leading?" doesn't match what users type — write what *they* would.
- **Stacking near-duplicates.** Two questions that differ only by a word add little. Use the slot for a different angle (price, comparison, durability, install).

Worked examples:

```jsonc
// featureCard fiber: "Dual-layer insulation, 24h cold / 12h hot"
{ "embedding": { "mode": "custom", "questions": [
  "How long does the bottle keep things cold?",
  "Will it keep coffee hot for a workday?",
  "Does it sweat or condense on the outside?",
  "Is the insulation vacuum sealed?"
]}}

// faq fiber: "Yes — top-rack dishwasher safe; hand-wash the lid."
{ "embedding": { "mode": "custom", "questions": [
  "Can I put it in the dishwasher?",
  "Is the lid dishwasher safe?",
  "Will dishwashing warp the bottle?",
  "How do I clean it?"
]}}

// pricing fiber: "Pro tier $29/mo, includes X / Y / Z"
{ "embedding": { "mode": "custom", "questions": [
  "How much does the Pro plan cost?",
  "What's included in Pro vs Free?",
  "Is there a monthly or annual option?",
  "Do you offer a free trial?"
]}}

// caseStudy fiber: "Acme cut churn 40% with our onboarding emails"
{ "embedding": { "mode": "custom", "questions": [
  "Has anyone like Acme used this?",
  "What kind of churn reduction is realistic?",
  "Do you have a B2B SaaS case study?",
  "How long did Acme take to see results?"
]}}
```

Rule of thumb: read the chunk, imagine three different people who'd want to find it, and write down what each of them would actually type. If you can't picture the searcher, fall back to `mode: "auto"` and let the LLM pick.

### 5d. Building threads

A thread is a title + ordered components. Components are either **fiber-backed** (pass `fiberIds` and the renderer hydrates slides from fibers) or **inline-authored** (carry their own content).

```bash
# Create the thread
TID=$(curl -s -X POST "https://api.oeave.com/agent/v1/projects/$PID/threads" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"title":"How it works","funnelStage":"education"}' | jq -r .data.id)

# Append components one at a time
curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/threads/$TID/components" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"component":{"kind":"text","richText":"<h2>Step by step</h2>"}}'

curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/threads/$TID/components" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"component":{"kind":"carousel","fiberIds":["f_step1","f_step2","f_step3"]}}'

curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/threads/$TID/components" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"component":{"kind":"cta","label":"Get yours","href":"https://brand.com/buy"}}'
```

Alternatively, include `components[]` in the initial POST. Component `id` is server-generated if omitted on every write path (per-component POST, thread POST with inline `components`, and thread `PATCH op:"replace"`) — grab it from the response.

**Always preserve component ids when round-tripping.** When you GET a thread, mutate components, and PATCH them back, keep each component's `id` field intact. The renderer keys its background-texture cache on `id`; a missing or empty `id` used to ship to the CDN and crash the embed at runtime. The API now backfills missing ids on write, but agent-side stability is still the cleaner contract — if you drop ids during a mutation, the next response will hand you fresh ones and any external link to a specific component (analytics, deep links) will break.

**Thread-level PATCH-replace** is the pragmatic way to edit a thread's components (see the last paragraph of this section for details):

```bash
curl -X PATCH "https://api.oeave.com/agent/v1/projects/$PID/threads/$TID" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -H "If-Match: $VERSION" \
  -d '{"op":"replace","components":[ /* full new list */ ]}'
```

**`funnelStage` values** (used by suggestions and thread ordering): `awareness`, `education`, `validation`, `decision`, `conversion`.

**Carousel / carouselHero only hydrate SLIDE MEDIA from fibers that actually carry media.** That means `image` (imageSrc), `video` (videoSrc), `threeDModel` (modelSrc), plus any fiber kind whose schema exposes an image-ish field and has it populated: `brandStory.image`, `featureCard.icon`, `useCase.image`, `testimonial.image`, `caseStudy.image`, `resource.image`, `pressMention.logo`, `behindTheScenes.media`. Purely text-shaped fibers — `faq`, `socialProof`, `coreValues`, `contactInfo`, `comparison`, `pricing`, `technicalSpecification`, `howItWorks`, `problemStatement`, `designPhilosophy`, `roadmap`, `targetAudience`, `benefitCard`, `setupGuide`, `timeline`, `legal`, `personalization`, `guarantee`, `bundle`, `availability`, `accessory`, `externalPage` — will still become slides, but with no background image. They render as captions on an empty surface and look sparse.

Choose the right component for the content:

| Content shape | Component | Notes |
|---|---|---|
| Short prose, value propositions, "why we built it" | `text` (`{kind:"text", richText:"..."}`) | **Default for simple text.** Auto-picks a background from `style.desktop.brandGraphic.images`. Override with `backgroundGraphics`. Prefer this over `cards` for narrative blocks. |
| Structured card grid (3–6 parallel tiles) | `cards` with inline `cards:[{title, richText}]` | Reserve for genuinely grid-shaped content. |
| Pricing tiers | `pricing` with inline `plans:[...]` | Not fiber-backed; inline plans. |
| Comparison matrix | `table` with inline `rows` | Also not fiber-backed. |
| Image / video / 3D story slides | `carousel` / `carouselHero` with `fiberIds` of image/video/3D fibers | Mix in screenshot fibers to carry the visual. |
| Standalone image or model | `image` / `model3d` with inline `src: {assetId}` | Single-slide form. |

**Hero anchors need a visual.** A `carouselHero` with only a title + a text-only fiber will render with no media. Pair every `carouselHero` with at least one image (or video / 3D) fiber in `fiberIds`, or upload an image and add it to the underlying fiber's optional `image` field.

**Starter thread shapes that work.** Use these as templates — they avoid the empty-media pitfall and land on funnel stages that the app already understands.

| Funnel stage | Good shape |
|---|---|
| `awareness` | `carouselHero`(hero image fibers) → `text`(problem framing) → `text`(headline stats) → `carousel`(3-5 product/mood images) → `text`(positioning) → `cta` |
| `education` | `carouselHero`(UI/product shots) → alternating `text` + `carousel` blocks, one per step → closing `text`(capability summary) → `cta` |
| `validation` | `carouselHero`(hero + one screenshot) → 2-3 `text` blocks ("us vs. …" narratives) → `carousel`(screenshots) → `text`(audience / outcomes) → `cta` |
| `decision` | `carouselHero`(hero + callout shot) → `pricing`(inline `plans`) → several `text` blocks (FAQ entries) → `cta` |
| `conversion` | `carouselHero`(hero + dashboards) → `text`(vision / roadmap) → `text`(values) → `carousel`(screenshots) → `text`(contact / skill pointer) → `cta` |

**`richText` accepted syntax.** The renderer parses a narrow HTML subset plus markdown. Reliable tags: `<b>` / `<strong>`, `<em>` / `<i>`, `<span style="color:…;font-size:…">`, `<ul>` / `<ol>` / `<li>`, `<br>`, and `<h1>`–`<h6>` (recognized at the block level in the `text` component). `<p>` is accepted as a paragraph separator but not preserved as markup. Other tags — including `<a>`, `<code>`, `<img>`, `<blockquote>` — are stripped. Markdown (`**bold**`, `*italic*`, `- bullets`) is converted to the supported subset before parsing.

**Editing a thread's components.** Prefer the thread-level replace over the per-component endpoints. `PATCH /threads/:tid/components/:cid` / `DELETE .../:cid` require a stable `cid`. Components written through the API now always carry an `id` in the GET response; legacy threads (or anything written before that change) may surface without one. If a `cid` is missing, fall back to `PATCH /threads/:tid` with `op: "replace"` and the full new `components` array (see the snippet above) — pass each component's existing `id` through unchanged so external references (analytics, deep links) keep working.

### 5e. Style

Style is `{desktop, mobile}`. PATCH deep-merges — send only the subtree you want to change and unrelated fields (e.g. `bodyFont`, `colors`) stay intact:

```bash
curl -X PATCH "https://api.oeave.com/agent/v1/projects/$PID/style" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"desktop":{"presentationMode":"feed","colors":{"primary":{"r":20,"g":40,"b":80,"a":1},"mode":"dark"}}}'
```

Asset references accept both forms — the bare `{assetId}` shorthand and the already-expanded `{_meta: {assetId}, srcSet, ...}` shape returned by GET. Round-tripping GET → mutate → PATCH works without manual cleanup.

**`brandGraphic.logo` has a nested variant shape, unlike `brandGraphic.images`:**

```jsonc
{
  "desktop": {
    "brandGraphic": {
      "logo":   { "color": { "assetId": "as_logo" } },   // note: wrapped in a color-variant key
      "images": [ { "assetId": "as_hero" }, { "assetId": "as_decor" } ]  // flat assetId refs
    }
  }
}
```

Images in brandGraphic need `width` and `height` set on the underlying asset (see §5a) — otherwise PATCH fails downstream with a schema validation error about a missing `height` on `srcSet[0]`, not on the style document itself.

Font files and brand graphic images are `{ assetId }` references — upload them first.

Import a website's visual identity (colors + fonts + logo) in one step:

```bash
curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/style/import-from-url" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://brand.com"}'
# 202 → poll /tasks/:id
```

### 5f. Acting on suggestions

Oeave runs a scheduled analytics job that surfaces suggestions on pro/growth tiers — content gaps, low-relevance searches, trending queries, etc. Agents can read them and mark them done, but cannot trigger new ones (those only come from the scheduled job).

```bash
# Active suggestions only (default)
curl -H "Authorization: Bearer $OEAVE_API_KEY" \
  "https://api.oeave.com/agent/v1/projects/$PID/suggestions" | jq

# Include history
curl -H "Authorization: Bearer $OEAVE_API_KEY" \
  "https://api.oeave.com/agent/v1/projects/$PID/suggestions?status=all"

# Mark one accepted after you've acted on it
curl -X PATCH "https://api.oeave.com/agent/v1/projects/$PID/suggestions/$SID" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"status":"accepted"}'

# Or dismiss one you don't intend to act on
curl -X PATCH "https://api.oeave.com/agent/v1/projects/$PID/suggestions/$SID" \
  -H "Authorization: Bearer $OEAVE_API_KEY" -H "Content-Type: application/json" \
  -d '{"status":"dismissed"}'
```

Each suggestion has `type`, `message`, `impact` (1–5), `effort` (1–5), and optional `linkedFiberIds` / `linkedThreadIds`. Use `impact/effort` to prioritize — items with high impact and low effort first. When your change addresses a suggestion, mark it `accepted` so the user's settings page reflects the work.

### 5g. Publishing

```bash
curl -X POST "https://api.oeave.com/agent/v1/projects/$PID/publish" \
  -H "Authorization: Bearer $OEAVE_API_KEY"
# 202 { productId, status: "publishing" }

# Later
curl "https://api.oeave.com/agent/v1/projects/$PID/publish" \
  -H "Authorization: Bearer $OEAVE_API_KEY"
# { publishedAt, version, publishedUrl, pageUrl, llmTxtUrl }
```

Publish rebuilds the embed bundle, re-uploads to R2, and purges the CDN. It's metered (`publishBuilds`). Don't publish after every tiny change — build up a meaningful diff first.

**Report the URLs back to the user once publish succeeds** — they'll want to verify and share:

- `pageUrl` — the standalone interactive product page the user can open or share.
- `publishedUrl` — the embed JS snippet URL to drop into their store's HTML (e.g. `<script src="…/embed.js"></script>`).
- `llmTxtUrl` — the `llms.txt` output for answer-engine visibility.

If the user hasn't seen the experience yet, point them at `pageUrl` before they publish to production — it's the fastest way to spot structural issues before they ship.

---

## 6. Polling async work

Any 202 response includes `pending[]`. Poll each task until it succeeds or fails:

```bash
curl "https://api.oeave.com/agent/v1/tasks/$TASK_ID" \
  -H "Authorization: Bearer $OEAVE_API_KEY"
# { status: "running", progress: 0.4, completedSteps: 4, totalSteps: 10 }
```

Rules:
- Only block on a task when downstream work requires its output (e.g. publishing right after a big embedding batch — wait for embeddings).
- For most fire-and-forget writes (create a fiber, then create the next fiber), skip polling.
- Use a 500ms–2s poll interval, back off to 5s for long tasks.

---

## 7. Quotas and rate limits

Every response carries:
- `X-RateLimit-Limit`, `X-RateLimit-Remaining` — per-minute burst.
- `X-Quota-<metric>-Remaining` — monthly budget for uploads, depth, embedding tokens, publish builds, etc.

**Self-pace based on these:**
- If `X-RateLimit-Remaining` drops below 5, pause for 60s before the next request.
- If any `X-Quota-*-Remaining` drops below 10% of limit, batch the remaining work and tell the user before continuing.

Errors to expect:
- `429 rate_limited` → wait `Retry-After` seconds, retry.
- `402 quota_exceeded` → monthly cap hit; tell the user which metric (`error.details.metric`) and the upgrade URL.
- `403 insufficient_scope` → the key doesn't have `error.details.requiredScope`. Tell the user to regenerate a key with that scope.
- `409 conflict` → `If-Match` version stale; GET the resource again and retry with the new version.

Always surface quota / scope errors to the user verbatim — don't silently retry.

---

## 8. A solid end-to-end play

Building from scratch, in order:
1. `GET /docs` and `GET /me` — confirm access and capture quotas.
2. `GET /projects` → pick project.
3. `GET /projects/:pid/assets` and `GET /projects/:pid/fibers` — survey.
4. Ask the user for content sources (website, repo, files, notes) — see §3b.
5. Upload hero/product images → PUT → finalize with `variants: ["depth"]`.
6. Create (or PATCH) the `productProfile` fiber — non-negotiable anchor. Do this before anything else.
7. Create supporting fibers comprehensively — `brandStory`, multiple `featureCard`s, `testimonial`s, `howItWorks`, `pricing`, `faq`, plus any of `caseStudy`, `pressMention`, `comparison`, `useCase`, `problemStatement`, `guarantee`, `targetAudience`, `coreValues`, `designPhilosophy`, `behindTheScenes`, `timeline` the sources support. More is better.
8. Ingest or PATCH style — colors, fonts, presentation mode, and `brandGraphic` (logo). Don't skip the logo.
9. Create threads per funnel stage — awareness (brandStory + hero), education (howItWorks + features), validation (testimonials + pressMention), decision (pricing + faq), conversion (cta + pricing).
10. Use `search:read` to sanity-check: `GET /projects/:pid/search/fibers?q=insulation` should return your `featureCard`.
11. Check `/projects/:pid/suggestions` — if there are active analytics-driven suggestions, address the high-impact/low-effort ones before publishing, then mark them `accepted`.
12. Publish.

---

## 9. Integrating the published embed in a host page (SSR / Next.js)

After publish, you may be asked to install the embed on the customer's own site (Next.js, Astro, Remix, plain HTML). The "publish snippet" tab on the project page generates the full HTML for them, but if you're wiring it up inside a JSX/TSX codebase yourself, mind these details — they're easy to get wrong and the failure modes are silent.

**Ship the whole bundle, not just the script.** The full merchant snippet is:

1. `<link rel="preconnect">` + `<link rel="dns-prefetch">` for `cdn.oeave.com` and `api.oeave.com`.
2. `<link rel="mcp-server-url">` to `https://mcp.oeave.com/<projectSlug>`.
3. `<link rel="agent-skill">` to the project's `skill.md`, plus a hidden visually-transparent `<a href>` to the same URL (crawlers without `<link>` support follow it).
4. `<link rel="alternate" type="text/plain">` to `llms.txt`, plus a hidden `<a href>` mirror.
5. `<link rel="sitemap" type="application/xml">` to `videos.xml`.
6. `<script type="application/ld+json">` with the Product + VideoObject graph.
7. The SSR'd `<section class="oeave oeave-inspiration" data-oeave-project="<slug>">` skeleton.
8. `<script defer src="https://cdn.oeave.com/<slug>/embed.js"></script>`.

Every block contributes to AEO/SEO. Don't drop the agent-skill / llms.txt / MCP links to "save bytes" — they're how AI agents discover the project.

**The SSR'd section *is* the mount.** The runtime's hydration lookup queries `section.oeave[data-oeave-project="<slug>"]` directly and attaches its input + thread-button listeners in place. You don't need a separate mount `<div>` anywhere on the page — drop any host markup that points at one and let the section's `data-oeave-project` attribute do the work. If no SSR section is shipped, the runtime falls back to `document.body` as a last resort, which is how you end up with the experience rendered at the bottom of the page.

**Source order matters: parse the script *after* the section.** If the `<script>` runs before the SSR section exists in the DOM, the runtime falls through to `document.body.appendChild` and the experience renders at the bottom of the page (orphaned from where you wanted it). In static HTML, putting the script tag immediately after the section is enough. In React, `<Script strategy="afterInteractive">` can race with hydration in subtle ways — the safest pattern is to render section + script as a single SSR HTML payload via `dangerouslySetInnerHTML` so the browser parses them in source order.

**SSR'd section MUST carry `data-oeave-project="<projectSlug>"`.** Without it, the hydration lookup misses, the runtime calls `create()` and *appends a fresh section to the page*, leaving the SSR skeleton orphaned. You'll see two stacked inspiration sections — typically one where you placed the markup and one at `<body>` bottom. The fix is just the attribute. (`buildInspirationBlock` already emits it when `weave.projectSlug` is set — make sure the slug is plumbed through.)

**Next.js / React recipe.** Render the entire bundle as one block via `dangerouslySetInnerHTML` and add `suppressHydrationWarning`:

```tsx
<div
  suppressHydrationWarning
  dangerouslySetInnerHTML={{ __html: oeaveEmbedHtml }}
/>
```

`suppressHydrationWarning` is required because the `defer` script attaches listeners and may set ARIA attrs on the SSR'd nodes *before* React's hydration pass — without the suppression you'll get a noisy hydration mismatch warning on every page load. Limitation: scripts inside `dangerouslySetInnerHTML` execute only on the *initial* HTML parse — they don't re-run on SPA route changes back to the same page. For a marketing landing page that's fine. For routes reachable via `<Link>` from elsewhere, use `next/script` placed *inline after* the mount/section instead of relying on dangerouslySetInnerHTML.

**Escape `</script` inside JSON-LD when nesting it in the bundle.** If you `JSON.stringify` the Product/VideoObject graph and inline it inside a `<script type="application/ld+json">` tag, any literal `</script` in a transcript or description will close the parent `<script>` early and break the page. Run the stringified output through `.replace(/<\/(script)/gi, '<\\/$1')` before inserting.

**Don't put `<link rel="preconnect">` etc. inside `<head>` *and* inside the body bundle.** Browsers honor preconnect from anywhere, but duplicating the link tags wastes a connection slot and pollutes the network panel. Pick one location: either keep the `<head>`-level metadata managed by the framework (Next.js `metadata` API, etc.) and keep the body bundle to just the JSON-LD + section + script, or move everything into the body bundle and don't duplicate. The publish snippet assumes everything-in-one-block; a Next.js codebase typically splits along framework lines (canonical/og/twitter via `metadata`, the rest via the bundle).

**Republish after changing project metadata.** If the customer renames the project, edits threads referenced by the SSR skeleton's `data-oeave-thread` buttons, or changes the trigger config — re-run `POST /publish` and have them re-copy the snippet. Stale `data-oeave-thread` ids fail silently (button click does nothing).

---

## 10. Don't

- Don't invent endpoints that aren't in `/docs`.
- Don't construct `srcSet` objects by hand — always use `{ assetId }`.
- Don't finalize an image asset without `width` and `height` — you cannot fix it later (see §5a).
- Don't send `kind` as a number; use the string name.
- Don't put text-only fibers (`faq`, `socialProof`, `coreValues`, `contactInfo`, `comparison`, `pricing`, `technicalSpecification`, etc.) into `carousel` / `carouselHero` — they render as captions on empty slides. Use `text` / `cards` / `pricing` / `table` instead (see §5d).
- Don't build a `carouselHero` with zero media-bearing fibers — the hero has no visual.
- Don't publish in a loop; it's expensive.
- Don't poll a task faster than once per 500ms.
- Don't retry 4xx errors automatically (except 429) — read the error and fix the request.
- Don't strip per-component `id` fields when round-tripping a thread (GET → mutate → PATCH `op:"replace"`). Pass each existing `id` through; the API will mint replacements if you drop them, but downstream references (analytics, deep links) will silently rebind.

---

## Reference

- Full endpoint list: `GET https://api.oeave.com/agent/v1/docs` under `endpoints`.
- Fiber shapes: `GET https://api.oeave.com/agent/v1/docs/fiber-kinds`.
- Component shapes: `GET https://api.oeave.com/agent/v1/docs/components`.
- Style outline: `GET https://api.oeave.com/agent/v1/docs` under `style`.
- This document: `https://www.oeave.com/oeave-agent-skill.md` (downloadable) — also served via `GET https://api.oeave.com/agent/v1/skill`.
