<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[XPLA Engineering]]></title><description><![CDATA[XPLA Engineering]]></description><link>https://xpla-engineering.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>XPLA Engineering</title><link>https://xpla-engineering.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 05:08:01 GMT</lastBuildDate><atom:link href="https://xpla-engineering.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Make AI API Docs Useful Before JavaScript Loads]]></title><description><![CDATA[I used to treat the documentation shell and the documentation content as the same thing. If the page looked complete after hydration, I considered it done.
That was too optimistic.
An API documentatio]]></description><link>https://xpla-engineering.hashnode.dev/ai-api-documentation-seo-before-javascript-loads</link><guid isPermaLink="true">https://xpla-engineering.hashnode.dev/ai-api-documentation-seo-before-javascript-loads</guid><category><![CDATA[Web Development]]></category><category><![CDATA[api]]></category><category><![CDATA[SEO]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[煜华]]></dc:creator><pubDate>Mon, 07 Sep 2026 13:29:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a96578e1147e4a7e52e02c4/a184cc89-811a-4204-abbf-cba48e6a9e59.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I used to treat the documentation shell and the documentation content as the same thing. If the page looked complete after hydration, I considered it done.</p>
<p>That was too optimistic.</p>
<p>An API documentation page has at least two jobs:</p>
<ol>
<li>answer a developer’s first question quickly; and</li>
<li>provide an interface for exploring the details.</li>
</ol>
<p>JavaScript is excellent for the second job. It is a fragile place to hide all of the first.</p>
<p>My current rule is simple:</p>
<blockquote>
<p>The server response must contain the stable answer. JavaScript may enhance it, but it should not be required to discover what the page is about.</p>
</blockquote>
<p>This is not a claim that client-rendered pages cannot be indexed. Search engines can process JavaScript. It is an engineering decision to make the essential information usable by more readers, crawlers, link unfurlers and failure modes.</p>
<h2>The six-field server HTML contract</h2>
<p>For every model page, I now expect the initial HTML to answer six questions:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Question it answers</th>
</tr>
</thead>
<tbody><tr>
<td>H1</td>
<td>Which model is this page about?</td>
</tr>
<tr>
<td>Summary</td>
<td>What is the model useful for?</td>
</tr>
<tr>
<td>Endpoint</td>
<td>Where does a request go?</td>
</tr>
<tr>
<td>Model ID</td>
<td>Which exact value belongs in the request?</td>
</tr>
<tr>
<td>Core specs</td>
<td>What constraints affect implementation?</td>
</tr>
<tr>
<td>Links</td>
<td>Where can the reader go next?</td>
</tr>
</tbody></table>
<p>If I remove the scripts and cannot answer those questions, the page is not finished.</p>
<p>The server output does not need every interactive control or every parameter row. It needs the stable facts that make the URL meaningful.</p>
<p>Here is the kind of structure I want in the response:</p>
<pre><code class="language-html">&lt;main&gt;
  &lt;nav aria-label="Breadcrumb"&gt;
    &lt;a href="/docs"&gt;Model docs&lt;/a&gt;
    &lt;span&gt;Gemini 3.1 Pro&lt;/span&gt;
  &lt;/nav&gt;

  &lt;article&gt;
    &lt;h1&gt;Gemini 3.1 Pro&lt;/h1&gt;
    &lt;p&gt;
      Model documentation with the request endpoint,
      model identifier and implementation constraints.
    &lt;/p&gt;

    &lt;dl&gt;
      &lt;dt&gt;Endpoint&lt;/dt&gt;
      &lt;dd&gt;&lt;code&gt;POST /v1/chat/completions&lt;/code&gt;&lt;/dd&gt;

      &lt;dt&gt;Model ID&lt;/dt&gt;
      &lt;dd&gt;&lt;code&gt;gemini-3.1-pro&lt;/code&gt;&lt;/dd&gt;
    &lt;/dl&gt;

    &lt;a href="/docs"&gt;Compare other model docs&lt;/a&gt;
  &lt;/article&gt;
&lt;/main&gt;
</code></pre>
<p>The exact values above must come from the product’s verified documentation or implementation. I never fill gaps with plausible model names.</p>
<h2>Why I separate the answer from the interface</h2>
<p>Interactive documentation tends to accumulate dependencies:</p>
<ul>
<li>a JavaScript bundle;</li>
<li>a router;</li>
<li>authentication state;</li>
<li>a syntax highlighter;</li>
<li>a request builder;</li>
<li>analytics;</li>
<li>sometimes a live model call.</li>
</ul>
<p>Any one of those can fail while the underlying documentation is still perfectly useful.</p>
<p>I therefore divide the page into two layers.</p>
<h3>Layer 1: the stable answer</h3>
<p>This is server-rendered:</p>
<ul>
<li>one descriptive H1;</li>
<li>a short, specific summary;</li>
<li>verified request coordinates;</li>
<li>the few constraints needed to avoid a bad first request;</li>
<li>normal HTML links to parent and related pages.</li>
</ul>
<h3>Layer 2: the working surface</h3>
<p>This is enhanced with JavaScript:</p>
<ul>
<li>parameter controls;</li>
<li>copy buttons;</li>
<li>language tabs;</li>
<li>request examples;</li>
<li>authentication-aware actions;</li>
<li>live task status.</li>
</ul>
<p>The second layer is allowed to be dynamic because the first layer already explains the page.</p>
<h2>I do not render a keyword list</h2>
<p>Server rendering is not a license to produce a wall of near-duplicate search phrases.</p>
<p>I write for one primary task. A model page can naturally mention its API, endpoint, model identifier and main capability because a developer needs those details. It should not repeat every possible variation of “best AI API” or create dozens of thin country copies.</p>
<p>My editing test is:</p>
<blockquote>
<p>Would this sentence still deserve to exist if search engines did not?</p>
</blockquote>
<p>If the answer is no, I cut it.</p>
<p>That test also helps with AI answer systems. A concise definition, a small table and a checkable procedure are easier to quote accurately than a long paragraph of promotional claims. That does not guarantee citation. It simply makes the source less ambiguous.</p>
<h2>The detail URL must fail honestly</h2>
<p>One mistake I see in documentation SPAs is returning a generic 200 page for every unknown route.</p>
<p>For a missing model document, I want:</p>
<ul>
<li>HTTP 404;</li>
<li>a clear “document not found” message;</li>
<li>no accidental canonical to a valid model;</li>
<li>a useful link back to the docs index.</li>
</ul>
<p>A soft 404 wastes both reader attention and crawl attention. More importantly, it makes automated QA harder because a broken link appears successful.</p>
<p>I include one malformed URL in every release check:</p>
<pre><code class="language-bash">curl -I "https://example.com/docs/not-a-real-model"
</code></pre>
<p>The test passes only when the response is genuinely missing, not when the application renders a friendly error inside an HTTP 200 shell.</p>
<h2>My release checklist</h2>
<p>I use two views of the same page.</p>
<h3>1. Inspect the raw response</h3>
<pre><code class="language-bash">curl -sS "https://example.com/docs/model-name" &gt; response.html
</code></pre>
<p>Then I check:</p>
<pre><code class="language-bash">rg -n "&lt;title|&lt;h1|canonical|Model ID|Endpoint" response.html
</code></pre>
<p>This catches the most common regression: the browser looks fine, but the facts exist only after JavaScript runs.</p>
<h3>2. Use the page as a reader</h3>
<p>I open the rendered page and check:</p>
<ul>
<li>the H1 is visible and unique;</li>
<li>the summary matches the page;</li>
<li>code is readable on mobile;</li>
<li>copy buttons do not replace selectable text;</li>
<li>links work without custom click handlers;</li>
<li>the interactive example is clearly separated from the factual reference.</li>
</ul>
<p>I also check the docs index. A perfect detail page is still difficult to discover if no crawlable page links to it.</p>
<h2>A real implementation boundary</h2>
<p>On XPLA’s model documentation, I applied this split to the public docs index and model pages. The initial response contains the page identity and stable model facts; the client interface keeps the richer parameter and example experience.</p>
<p>You can inspect the <a href="https://xplaai.com/docs">model documentation index</a> and a <a href="https://xplaai.com/docs/gemini-3-1-pro.html">Gemini 3.1 Pro documentation page</a>. Those links are examples of the structure, not evidence of guaranteed ranking or traffic.</p>
<p>The useful part of this pattern is that it scales as the model catalog changes:</p>
<ul>
<li>content fields have an explicit contract;</li>
<li>interactive components can evolve independently;</li>
<li>missing routes can be tested;</li>
<li>every release can be verified from the outside.</li>
</ul>
<h2>What I would not do</h2>
<p>I would not:</p>
<ul>
<li>render only a title and call the page “SEO-ready”;</li>
<li>place a huge hidden copy of the client interface in the HTML;</li>
<li>create one URL per keyword synonym;</li>
<li>use a canonical tag to disguise duplicate community posts;</li>
<li>publish unverified model IDs or capabilities;</li>
<li>promise that server rendering creates rankings or AI citations.</li>
</ul>
<p>The goal is not to manufacture more indexable text. The goal is to make each useful URL contain a complete, honest first answer.</p>
<h2>Frequently asked questions</h2>
<h3>Do API docs need full server-side rendering?</h3>
<p>Not always. Static generation, server rendering and carefully designed pre-rendering can all work. I care about the resulting response: the stable answer and links should be present without depending on client-side execution.</p>
<h3>Should every parameter be in the initial HTML?</h3>
<p>No. Put the fields needed to identify and start evaluating the API in the initial response. Large parameter explorers can remain interactive, provided essential facts are not hidden inside them.</p>
<h3>Does this guarantee Google or Bing indexing?</h3>
<p>No. Indexing and ranking depend on many signals. This pattern improves accessibility and reduces rendering dependencies; it is not a ranking guarantee.</p>
<h3>Does it guarantee inclusion in AI answers?</h3>
<p>No. Clear definitions and checkable facts may make a page easier to interpret, but no publisher can promise citation by an AI system.</p>
<h3>What should I test first?</h3>
<p>Fetch the raw HTML for one important detail page. If its H1, summary, endpoint, model ID and a parent link are missing, start there.</p>
<h2>Sources</h2>
<ul>
<li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics">Google Search Central: JavaScript SEO basics</a></li>
<li><a href="https://developers.google.com/search/docs/crawling-indexing/links-crawlable">Google Search Central: Make your links crawlable</a></li>
</ul>
<p>Disclosure: I work on XPLA. The checklist is the process I use; the XPLA links are implementation examples.</p>
]]></content:encoded></item><item><title><![CDATA[A Shot Queue for Veo 3.1 Product Videos]]></title><description><![CDATA[I would not give one AI video task responsibility for product identity, a mechanism demonstration, a lifestyle scene and the finished edit.
The problem is not only output quality. A large prompt produ]]></description><link>https://xpla-engineering.hashnode.dev/a-shot-queue-for-veo-3-1-product-videos</link><guid isPermaLink="true">https://xpla-engineering.hashnode.dev/a-shot-queue-for-veo-3-1-product-videos</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[AI]]></category><category><![CDATA[ecommerce]]></category><dc:creator><![CDATA[煜华]]></dc:creator><pubDate>Fri, 04 Sep 2026 11:44:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a96578e1147e4a7e52e02c4/8eda0395-856d-4643-882f-94b796257eda.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I would not give one AI video task responsibility for product identity, a mechanism demonstration, a lifestyle scene and the finished edit.</p>
<p>The problem is not only output quality. A large prompt produces a large failure surface.</p>
<p>My proposed shot queue gives every item one buyer question and one terminal review decision. The schema is an illustrative design, not a production benchmark or a report of a paid generation experiment.</p>
<blockquote>
<p>I work on XPLA. The contract details below describe the XPLA wrapper checked against public documentation on September 4, 2026, not Google's native API and not a price, speed, access or fidelity guarantee.</p>
</blockquote>
<h2>Queue schema</h2>
<pre><code class="language-ts">type ShotRole = "evidence" | "action" | "context";
type ReviewDecision = "accept" | "repair" | "reject";

type ShotJob = {
  shotId: string;
  sku: string;
  variant: string;
  role: ShotRole;
  buyerQuestion: string;
  referenceMode: "frame" | "ingredient";
  references: Array&lt;{
    assetId: string;
    rights: "approved" | "hold";
    proves: string[];
  }&gt;;
  mustPreserve: string[];
  allowedMotion: string[];
  prohibitedImplications: string[];
  acceptIf: string[];
  repairIf: string[];
  rejectIf: string[];
};
</code></pre>
<p>The shot cannot enter <code>approved</code> while:</p>
<ul>
<li>its buyer question is empty;</li>
<li>a reference is on rights hold;</li>
<li>the variant is ambiguous;</li>
<li><code>mustPreserve</code> is empty;</li>
<li>no review criteria exist.</li>
</ul>
<h2>Three roles, three failure surfaces</h2>
<p>An <strong>evidence shot</strong> establishes identity. It keeps the product visible and motion restrained.</p>
<p>An <strong>action shot</strong> demonstrates one mechanism or interaction.</p>
<p>A <strong>context shot</strong> supplies the environment and use case.</p>
<p>The order matters. I review evidence before action and action before atmosphere. A wrong product in a beautiful context shot is still a wrong product.</p>
<h2>Motion budget</h2>
<p>I classify motion as:</p>
<pre><code class="language-ts">type MotionPlan = {
  productOrHand: "none" | "low" | "high";
  camera: "none" | "low" | "high";
  environment: "none" | "low" | "high";
};
</code></pre>
<p>The application allows no more than one <code>high</code>.</p>
<pre><code class="language-ts">function validateMotion(plan: MotionPlan) {
  const highCount = Object.values(plan)
    .filter(value =&gt; value === "high").length;

  return highCount &lt;= 1
    ? []
    : ["Use only one high-variance motion source per shot"];
}
</code></pre>
<p>This is not a model restriction. It is a debugging restriction. If the hand, camera and environment all move aggressively, a failed mechanism becomes hard to diagnose.</p>
<h2>Wrapper contract gate</h2>
<p>The current XPLA model name is <code>veo-3.1-fast</code> and the route is <code>POST /v1/videos</code>.</p>
<p>The inspected wrapper accepts:</p>
<ul>
<li>fixed eight-second tasks;</li>
<li>720p or 1080p;</li>
<li>16:9 or 9:16;</li>
<li><code>frame</code> mode with up to two ordered image references;</li>
<li><code>ingredient</code> mode with up to three references.</li>
</ul>
<p>It rejects reference-video input. Native Google Veo documentation may describe capabilities or fields that do not belong in this wrapper.</p>
<p>The queue validates its access-layer contract before submission:</p>
<pre><code class="language-ts">function validateReferenceCount(job: ShotJob) {
  const max = job.referenceMode === "frame" ? 2 : 3;

  if (job.references.length &gt; max) {
    return [`${job.referenceMode} mode accepts at most ${max} references`];
  }

  if (job.references.some(ref =&gt; ref.rights !== "approved")) {
    return ["Every reference must pass the rights gate"];
  }

  return [];
}
</code></pre>
<h2>State belongs to the shot intent</h2>
<p>The generation record is separate from the creative brief:</p>
<pre><code class="language-ts">type TaskState =
  | "approved"
  | "submitted"
  | "transport_unknown"
  | "completed"
  | "failed"
  | "reviewed";

type GenerationTask = {
  shotId: string;
  generationIntent: "first_calibration" | "repair" | "new_creative";
  requestHash: string;
  taskId: string | null;
  state: TaskState;
  attempt: number;
  nextAction: "submit" | "poll" | "reconcile" | "review";
};
</code></pre>
<p>A transport timeout moves the record to <code>transport_unknown</code>. It does not change <code>generationIntent</code> to <code>new_creative</code>.</p>
<p>The application reconciles the original task before creating another paid intent. Otherwise one ambiguous retry can become two valid outputs with no obvious explanation.</p>
<h2>QA before editing</h2>
<p>My review order is:</p>
<ol>
<li>SKU and variant identity;</li>
<li>geometry and product count;</li>
<li>label and text;</li>
<li>mechanism and contact points;</li>
<li>physical plausibility;</li>
<li>crop and channel fit;</li>
<li>lighting and rhythm.</li>
</ol>
<p>The output becomes:</p>
<pre><code class="language-ts">type Review = {
  shotId: string;
  decision: ReviewDecision;
  failedChecks: string[];
  repairScope: string[];
  reviewer: string;
  reviewedAt: string;
};
</code></pre>
<p><code>repair</code> is deliberately narrow. It fits cases where the product is correct and a local layer—crop, timing, caption or sound—can be fixed.</p>
<p>If the label, count, mechanism or product geometry changed, the decision is <code>reject</code>.</p>
<h2>Editing is a different stage</h2>
<p>An eight-second model task is a production unit, not necessarily the final ad.</p>
<p>The editing stage may trim approved clips, add verified captions, place an approved voice track and licensed sound, and assemble the final sequence.</p>
<p>It should also preserve lineage:</p>
<pre><code class="language-text">source asset -&gt; shot card -&gt; normalized request -&gt; task ID
             -&gt; review decision -&gt; timeline clip
</code></pre>
<p>No paid generation was run for this article. The schema and diagram explain a workflow; they are not output-quality evidence or a customer case.</p>
<p>The current fields and limitations are documented in the <a href="https://xplaai.com/en-us/api/veo-3-1-fast/">XPLA Veo 3.1 Fast guide</a>. That English documentation page currently returns <code>noindex,follow</code>; this link is for readers, not a claim of indexation or ranking.</p>
<p>My stop rule: after two evidence-shot failures on the same non-negotiable product fact, return to the source and shot design. Do not scale the queue.</p>
]]></content:encoded></item><item><title><![CDATA[A Request State Machine for GPT Image 2 Product Images]]></title><description><![CDATA[The most expensive image API bug is not always a failed request. Sometimes it
is a request that succeeds twice.
I saw why a normal “retry” button is too vague for a GPT Image 2 API workflow:
a timeout]]></description><link>https://xpla-engineering.hashnode.dev/a-request-state-machine-for-gpt-image-2-product-images</link><guid isPermaLink="true">https://xpla-engineering.hashnode.dev/a-request-state-machine-for-gpt-image-2-product-images</guid><category><![CDATA[gpt-image-2]]></category><category><![CDATA[api]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[ecommerce]]></category><dc:creator><![CDATA[煜华]]></dc:creator><pubDate>Wed, 02 Sep 2026 11:51:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a96578e1147e4a7e52e02c4/93f45acd-e43f-46e3-9624-f68f27262910.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The most expensive image API bug is not always a failed request. Sometimes it
is a request that succeeds twice.</p>
<p>I saw why a normal “retry” button is too vague for a GPT Image 2 API workflow:
a timeout tells the client that the result is unknown. It does not prove that
no generation happened.</p>
<p>So I model generation as a small state machine and put contract validation in
front of it.</p>
<blockquote>
<p>I work on XPLA. This is the application-layer pattern I use, not an
independent product comparison or a price, latency, access or fidelity
guarantee.</p>
</blockquote>
<h2>State belongs to the intent, not the HTTP attempt</h2>
<p>I persist the record before submitting:</p>
<pre><code class="language-json">{
  "generation_id": "img-job-20260902-001",
  "intent": "new_calibration",
  "request_hash": "sha256-of-normalized-request",
  "state": "submitted",
  "attempt": 1,
  "result_state": "unknown",
  "next_action": "check_before_repeating"
}
</code></pre>
<p>Then I use explicit transitions:</p>
<pre><code class="language-text">draft -&gt; approved -&gt; submitted -&gt; completed
completed -&gt; accepted | repair | rejected

submitted -&gt; transport_unknown
transport_unknown -&gt; reconcile
reconcile -&gt; completed | safe_retry | manual_review
</code></pre>
<p>An intentional second creative gets a new generation ID. A transport retry
keeps the original intent. This makes duplicates visible in logs and billing
review instead of turning them into mysterious extra outputs.</p>
<h2>Contract validation happens before state submission</h2>
<p>The form is driven by a contract registry:</p>
<pre><code class="language-js">const contracts = {
  "gpt-image-2": {
    endpoint: "/v1/images/generations",
    allowed: new Set([
      "model", "prompt", "image", "images",
      "size", "aspectRatio", "quality", "replyType", "n"
    ]),
    forbidden: new Set(["imageSize"])
  }
};
</code></pre>
<p>I reject unknown fields rather than deleting them:</p>
<pre><code class="language-js">function validateFields(body, contract) {
  const errors = [];

  for (const key of Object.keys(body)) {
    if (!contract.allowed.has(key)) {
      errors.push(`Unsupported field: ${key}`);
    }
  }

  for (const key of contract.forbidden) {
    if (body[key] !== undefined) {
      errors.push(`Forbidden field: ${key}`);
    }
  }

  return errors;
}
</code></pre>
<p>Silent deletion makes debugging harder because the UI says one thing while
the submitted request says another.</p>
<h2>One request gets one dimension decision</h2>
<p>I validate dimensions separately:</p>
<pre><code class="language-js">function validateDimensions(body) {
  if (body.imageSize !== undefined) {
    return ["imageSize does not belong to this standard contract"];
  }

  if (body.size !== undefined &amp;&amp; body.aspectRatio !== undefined) {
    return ["Choose size or aspectRatio, not both"];
  }

  return [];
}
</code></pre>
<p>I also persist the selected orientation with the job. Otherwise the reviewer
cannot distinguish a model failure from a request-builder failure.</p>
<h2>Reference inputs carry rights and product facts</h2>
<p>The job owns a companion record:</p>
<pre><code class="language-json">{
  "source_type": "merchant_upload",
  "rights_state": "confirmed_for_internal_calibration",
  "sku": "merchant-sku-104",
  "variant": "matte-black-500ml",
  "must_preserve": [
    "one unit",
    "matte black body",
    "silver cap",
    "label geometry"
  ],
  "allowed_changes": ["background", "surface", "lighting"]
}
</code></pre>
<p>The generation cannot move to <code>approved</code> if rights or exact product identity
are unresolved. A public listing photo is not automatically licensed for AI
transformation.</p>
<h2>Calibration is a state, not a batch size</h2>
<p>My first approved run has one source, one output and one reviewer. The result
moves to:</p>
<ul>
<li><code>accepted</code> when the product facts and release criteria pass;</li>
<li><code>repair</code> when a bounded change can fix the job;</li>
<li><code>rejected</code> when rights, contract expectations or product truth fail.</li>
</ul>
<p>The QA record checks count, color, label, claims, orientation and crop. A
visually impressive output can still be commercially unusable.</p>
<h2>Failure ownership</h2>
<table>
<thead>
<tr>
<th>Class</th>
<th>Example</th>
<th>Owner action</th>
</tr>
</thead>
<tbody><tr>
<td>Client</td>
<td>empty prompt</td>
<td>fix before submit</td>
</tr>
<tr>
<td>Contract</td>
<td>unsupported field</td>
<td>reject locally</td>
</tr>
<tr>
<td>Access</td>
<td>unauthorized key</td>
<td>stop and repair access</td>
</tr>
<tr>
<td>Transport</td>
<td>response lost</td>
<td>reconcile intent</td>
</tr>
<tr>
<td>Provider</td>
<td>transient failure</td>
<td>bounded retry</td>
</tr>
<tr>
<td>Safety</td>
<td>risky request</td>
<td>stop or revise</td>
</tr>
<tr>
<td>Output QA</td>
<td>product altered</td>
<td>repair or reject</td>
</tr>
</tbody></table>
<p>Collapsing these into one <code>failed</code> state removes the information needed for
the next safe action.</p>
<h2>The boundary I ship</h2>
<pre><code class="language-js">function preflight(body, rightsRecord) {
  const contract = contracts[body.model];
  const errors = [];

  if (!contract) errors.push(`Unknown model: ${body.model}`);
  if (!body.prompt?.trim()) errors.push("prompt is required");
  if (body.n !== undefined &amp;&amp; body.n !== 1) {
    errors.push("Create a separate intent for each candidate");
  }
  if (body.replyType !== undefined &amp;&amp; body.replyType !== "json") {
    errors.push('replyType must be "json"');
  }

  if (contract) {
    errors.push(...validateFields(body, contract));
    errors.push(...validateDimensions(body));
  }

  if ((body.image || body.images) &amp;&amp;
      rightsRecord?.rights_state !== "confirmed_for_internal_calibration") {
    errors.push("Reference rights are not confirmed");
  }

  return { ok: errors.length === 0, errors };
}
</code></pre>
<p>It is not a universal SDK. I add file, URL, privacy, policy, account and
storage validation around it.</p>
<p>The useful order is:</p>
<pre><code class="language-text">contract -&gt; preflight -&gt; approved intent -&gt; submitted
-&gt; reconcile if unknown -&gt; one calibration -&gt; product QA -&gt; scale
</code></pre>
<p>For the currently exposed XPLA request shape, see the
<a href="https://xplaai.com/en-us/api/gpt-image-2/">GPT Image 2 API guide</a>. Verify the
live model contract again before a production integration.</p>
]]></content:encoded></item><item><title><![CDATA[How I Design Comparable TikTok Shop Product Snapshots]]></title><description><![CDATA[When I build a product-research workflow, the most dangerous bug is not always a failed request. It is a successful request whose output gets mislabeled.
A cumulative sales field can be useful. It bec]]></description><link>https://xpla-engineering.hashnode.dev/how-i-design-comparable-tiktok-shop-product-snapshots</link><guid isPermaLink="true">https://xpla-engineering.hashnode.dev/how-i-design-comparable-tiktok-shop-product-snapshots</guid><category><![CDATA[TikTok Shop]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[ecommerce]]></category><dc:creator><![CDATA[煜华]]></dc:creator><pubDate>Wed, 02 Sep 2026 04:52:17 GMT</pubDate><content:encoded><![CDATA[<p><img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/e5delr5ze1zcb649gura.png" alt="Two comparable TikTok Shop product snapshots flow into comparison and human review, showing why one snapshot is not a trend." /></p>
<p>When I build a product-research workflow, the most dangerous bug is not always a failed request. It is a successful request whose output gets mislabeled.</p>
<p>A cumulative sales field can be useful. It becomes misleading when a pipeline silently turns it into “seven-day sales” or treats one collection as a trend.</p>
<p>I use a small comparison contract to prevent that.</p>
<h2>The contract</h2>
<p>Every observation needs:</p>
<pre><code class="language-ts">type ProductSnapshot = {
  productId: string;
  market: string;
  currency: string;
  variantKey: string | null;
  metricName: string;
  metricDefinition: string;
  value: number | null;
  collectedAt: string;
  sourceUrl: string;
};
</code></pre>
<p>The missing value is intentional. Unknown is not zero.</p>
<p>Before comparing two snapshots, I require:</p>
<pre><code class="language-ts">function isComparable(a: ProductSnapshot, b: ProductSnapshot) {
  return (
    a.productId === b.productId &amp;&amp;
    a.market === b.market &amp;&amp;
    a.currency === b.currency &amp;&amp;
    a.variantKey === b.variantKey &amp;&amp;
    a.metricName === b.metricName &amp;&amp;
    a.metricDefinition === b.metricDefinition
  );
}
</code></pre>
<p>In production I also version the collection method. Two records can share a field name while coming from different parsing or normalization logic.</p>
<h2>Abort conditions are part of the feature</h2>
<p>If the product changed from one unit to a bundle, the records are not comparable.</p>
<p>If the source changed the field definition, the records are not comparable.</p>
<p>If a missing value became zero during serialization, the comparison is corrupted.</p>
<p>If the timestamp or time zone is absent, I cannot calculate a defensible rate.</p>
<p>The pipeline needs a non-success state:</p>
<pre><code class="language-ts">type ComparisonState =
  | "comparable"
  | "hold_identity_changed"
  | "hold_definition_changed"
  | "hold_missing_value"
  | "hold_missing_time";
</code></pre>
<p>Returning <code>hold</code> is not a workflow failure. It protects downstream decisions.</p>
<h2>Calculate a difference, not a forecast</h2>
<p>For comparable cumulative values:</p>
<pre><code class="language-ts">const difference = later.value - earlier.value;
const elapsedMs =
  Date.parse(later.collectedAt) - Date.parse(earlier.collectedAt);
</code></pre>
<p>I store both the raw difference and the elapsed time. I label the result <code>snapshot_difference</code>.</p>
<p>I do not rename it <code>demand</code>, <code>growth</code>, or <code>expected_sales</code>.</p>
<p>Two observations do not explain promotion changes, reporting delays, inventory, listing edits, or source behavior. They support a bounded observation, not a causal claim.</p>
<h2>Keep source facts separate from review</h2>
<p>The data record answers what the source returned.</p>
<p>A separate review record answers:</p>
<ul>
<li>Does the exact product identity still match?</li>
<li>Is the category or product restricted?</li>
<li>Are sourcing and fulfillment known?</li>
<li>Are claims and media rights supported?</li>
<li>Can the product be demonstrated truthfully?</li>
<li>Does this candidate deserve another snapshot or a small creative test?</li>
</ul>
<p>I do not merge those questions into an opaque “winner score.” The reviewer should be able to see why a candidate moved forward—or why it stopped.</p>
<h2>Connect research to creative only after review</h2>
<p>An approved candidate becomes a creative brief with:</p>
<ul>
<li>one customer problem;</li>
<li>supportable product facts;</li>
<li>required demonstrations;</li>
<li>objections;</li>
<li>prohibited claims;</li>
<li>multiple creative hypotheses;</li>
<li>an owner and stop condition.</li>
</ul>
<p>This separation matters for custom workflows. Data collection, product approval, and creative generation should be distinct states with explicit handoffs.</p>
<p>I work on XPLA. Its public TikTok Shop data API guide documents product search and supported product details, including the boundary that cumulative values should not be presented as a recent time window:</p>
<p><a href="https://xplaai.com/en-us/api/tiktok-shop-data/">https://xplaai.com/en-us/api/tiktok-shop-data/</a></p>
<p>The endpoint is only one component. The durable part is the contract around identity, time, metric meaning, and review.</p>
<p>My final assertion is deliberately modest:</p>
<pre><code class="language-text">one snapshot = baseline
two comparable snapshots = observed difference
human review = decision
</code></pre>
<p>That is enough to keep a product-research pipeline honest—and useful.</p>
]]></content:encoded></item><item><title><![CDATA[Unified AI API Tutorial: Keep Model Contracts Explicit]]></title><description><![CDATA[Why I wrote this
When I review a unified AI API, I do not start by counting model names. I start
by checking which contracts are explicit, which jobs are asynchronous, and
where the application must k]]></description><link>https://xpla-engineering.hashnode.dev/unified-ai-api-tutorial-keep-model-contracts-explicit</link><guid isPermaLink="true">https://xpla-engineering.hashnode.dev/unified-ai-api-tutorial-keep-model-contracts-explicit</guid><category><![CDATA[AI]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[煜华]]></dc:creator><pubDate>Tue, 01 Sep 2026 07:09:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a96578e1147e4a7e52e02c4/7b6ee309-4322-4ec4-87da-125c65409935.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Why I wrote this</h2>
<p>When I review a unified AI API, I do not start by counting model names. I start
by checking which contracts are explicit, which jobs are asynchronous, and
where the application must keep its own validation and review gates. This is
the contract-first checklist I use to separate a useful access layer from a
misleading "one schema for everything" abstraction.</p>
<h2>Direct answer</h2>
<p>A useful <strong>unified AI API</strong> gives your application one access layer without
pretending that every model behaves the same.</p>
<p>Centralizing the account, base URL, authentication pattern, and route discovery
can reduce integration overhead. But image, video, speech, chat, and commerce
tasks still have different parameters, response patterns, and review
requirements. A reliable implementation keeps those contracts explicit.</p>
<p><img src="https://xplaai.com/media/unified-ai-api-request-lifecycle-en-us.svg" alt="Contract-first unified AI API flow from one Bearer token to separate chat, image, speech, video, and commerce route adapters." /></p>
<p>In this guide, I show a contract-first workflow using XPLA's documented route
families as the example. The method also applies when you are designing your
own multi-model gateway.</p>
<h2>What should a unified AI API actually unify?</h2>
<p>The word <em>unified</em> is useful when it describes:</p>
<ul>
<li>one account and access layer;</li>
<li>one base URL;</li>
<li>one Bearer-token pattern;</li>
<li>one place to discover current models;</li>
<li>explicit routes for different task families;</li>
<li>a consistent place to check current documentation.</li>
</ul>
<p>It becomes misleading when it implies:</p>
<ul>
<li>one universal request body;</li>
<li>identical model parameters;</li>
<li>identical synchronous or asynchronous behavior;</li>
<li>a fixed catalog available to every account;</li>
<li>guaranteed failover, lower cost, latency, throughput, or uptime.</li>
</ul>
<p>The safest architecture unifies access while preserving model contracts.</p>
<h2>1. Define the application job before choosing a model</h2>
<p>Start with the result your user needs:</p>
<ul>
<li>What is the input?</li>
<li>What output must be delivered?</li>
<li>Is a response needed immediately?</li>
<li>Can the task run asynchronously?</li>
<li>Does the task contain private media?</li>
<li>What rights and retention rules apply?</li>
<li>What must a human review before the result is used?</li>
</ul>
<p>This decision should select a route family before it selects a model name.</p>
<table>
<thead>
<tr>
<th>Application job</th>
<th>XPLA route</th>
<th>Pattern</th>
</tr>
</thead>
<tbody><tr>
<td>Discover models available to a token</td>
<td><code>GET /v1/models</code></td>
<td>synchronous JSON</td>
</tr>
<tr>
<td>Generate or edit an image</td>
<td><code>POST /v1/images/generations</code></td>
<td>synchronous for supported contracts</td>
</tr>
<tr>
<td>Create a video task</td>
<td><code>POST /v1/videos</code></td>
<td>asynchronous</td>
</tr>
<tr>
<td>Check a video task</td>
<td><code>GET /v1/videos/{task_id}</code></td>
<td>poll by task ID</td>
</tr>
<tr>
<td>Retrieve completed video content</td>
<td><code>GET /v1/videos/{task_id}/content</code></td>
<td>authenticated content read</td>
</tr>
<tr>
<td>Run a chat completion</td>
<td><code>POST /v1/chat/completions</code></td>
<td>completion or streaming contract</td>
</tr>
<tr>
<td>Generate speech</td>
<td><code>POST /v1/audio/speech</code></td>
<td>text-to-speech contract</td>
</tr>
<tr>
<td>Search TikTok Shop data</td>
<td><code>GET /v1/tiktok/shop/search</code></td>
<td>commerce-data query</td>
</tr>
<tr>
<td>Get one TikTok product</td>
<td><code>GET /v1/tiktok/product</code></td>
<td>product lookup</td>
</tr>
<tr>
<td>Parse an authorized public video</td>
<td><code>POST /v1/video/parse</code></td>
<td>public-video analysis</td>
</tr>
</tbody></table>
<p>The table is a route-selection aid, not a guarantee that every model is enabled
for every account. Check the current documentation before implementation.</p>
<h2>2. Discover models instead of copying an old model name</h2>
<p>Keep the real API key outside source code, screenshots, browser JavaScript, and
shared prompts. In a server-side shell, assign it to an environment variable:</p>
<pre><code class="language-bash">export XPLA_API_KEY="replace_with_your_key"

curl --request GET \
  --url https://xplaai.com/v1/models \
  --header "Authorization: Bearer $XPLA_API_KEY"
</code></pre>
<p>Treat the response as a current capability input, not a permanent catalog.
Before you expose a model in your product, record:</p>
<ol>
<li>its exact public name;</li>
<li>the endpoint family it belongs to;</li>
<li>the accepted fields;</li>
<li>whether the result is synchronous or asynchronous;</li>
<li>the last contract-review date;</li>
<li>the output checks required by your application.</li>
</ol>
<p>Do not silently replace one model with another when their parameters differ.</p>
<h2>3. Build a model-contract registry</h2>
<p>A route adapter should not accept every possible field and forward it blindly.
Instead, keep an explicit registry:</p>
<pre><code class="language-ts">type ModelContract = {
  route: '/v1/images/generations' | '/v1/videos';
  allowedFields: readonly string[];
  result: 'synchronous' | 'asynchronous';
  reviewedAt: string;
};

const contracts: Record&lt;string, ModelContract&gt; = {
  'gpt-image-2': {
    route: '/v1/images/generations',
    allowedFields: ['model', 'prompt', 'size', 'images', 'quality'],
    result: 'synchronous',
    reviewedAt: '2026-08-31',
  },
  'veo-3.1-fast': {
    route: '/v1/videos',
    allowedFields: [
      'model',
      'prompt',
      'seconds',
      'resolution',
      'ratio',
      'images',
      'metadata',
    ],
    result: 'asynchronous',
    reviewedAt: '2026-08-31',
  },
};
</code></pre>
<p>This is an illustrative registry based on a dated contract review. Generate
production values from the documentation and tests your team has actually
approved.</p>
<p>The review date should trigger revalidation. It is not decorative metadata.</p>
<h2>4. Separate synchronous results from asynchronous tasks</h2>
<p>Supported image-generation contracts can return a result in the initial
response. Video creation requires a task lifecycle:</p>
<ol>
<li>send <code>POST /v1/videos</code>;</li>
<li>persist the returned task ID;</li>
<li>poll <code>GET /v1/videos/{task_id}</code> with bounded intervals;</li>
<li>stop polling on a documented terminal state;</li>
<li>retrieve ready content through
<code>GET /v1/videos/{task_id}/content</code>;</li>
<li>send the output to rights and quality review.</li>
</ol>
<p>Do not keep a task ID only in browser memory. A refresh, worker restart, or
temporary network failure should not make the application lose the job.</p>
<h2>5. Normalize errors without erasing the original cause</h2>
<p>Your product can present a small internal error taxonomy, but preserve the
original route, public model name, status, and task ID.</p>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Application action</th>
<th>Avoid</th>
</tr>
</thead>
<tbody><tr>
<td><code>401</code></td>
<td>stop and request a valid server-side token</td>
<td>sending the key to client analytics</td>
</tr>
<tr>
<td><code>400</code> or validation error</td>
<td>show the rejected field and selected contract</td>
<td>retrying the same invalid body</td>
</tr>
<tr>
<td><code>429</code></td>
<td>use bounded backoff and show availability state</td>
<td>unlimited concurrent retries</td>
</tr>
<tr>
<td>terminal task failure</td>
<td>preserve task ID and reason</td>
<td>reporting success because task creation returned an ID</td>
</tr>
<tr>
<td>authenticated or temporary content URL</td>
<td>retrieve through the documented flow</td>
<td>treating it as a permanent public asset</td>
</tr>
<tr>
<td>unsupported region or account</td>
<td>disable the action and explain the requirement</td>
<td>implying universal availability</td>
</tr>
</tbody></table>
<p>A gateway does not remove upstream or model failure modes. It gives the
application one public access boundary from which to handle them.</p>
<h2>6. Add observability before adding more models</h2>
<p>For each request, consider recording:</p>
<ul>
<li>an internal request ID with no customer secret;</li>
<li>route and public model name;</li>
<li>timestamp and application version;</li>
<li>an input hash instead of private raw media where possible;</li>
<li>task ID for asynchronous work;</li>
<li>current state and terminal reason;</li>
<li>output location and retention policy;</li>
<li>retry count;</li>
<li>review decision.</li>
</ul>
<p>Do not log a full authorization header, API key, private signed URL, or
unrestricted customer prompt by default.</p>
<h2>7. Treat API success and production approval as separate gates</h2>
<p>A <code>200</code> response or a completed task proves that the technical request
finished. It does not prove that:</p>
<ul>
<li>product identity is accurate;</li>
<li>text and claims are correct;</li>
<li>the media is licensed for its intended use;</li>
<li>the result fits the destination platform;</li>
<li>retention and privacy requirements were met.</li>
</ul>
<p>Merchant image and video workflows need a second approval gate for product
truth, rights, output quality, and storage.</p>
<h2>When should you use an API instead of a packaged workflow?</h2>
<p>Use the raw API when your team wants to own:</p>
<ul>
<li>the user interface and database;</li>
<li>the contract registry;</li>
<li>task queues and retry behavior;</li>
<li>storage and retention;</li>
<li>approvals and cost controls;</li>
<li>output QA and support.</li>
</ul>
<p>Use a packaged Commerce Skill when you want a repeatable workflow that
coordinates multiple steps and reports its evidence and approval gates. A
Skill does not eliminate model cost, rights review, or human judgment; it
changes how the workflow is organized.</p>
<h2>Limitations to plan for</h2>
<ul>
<li>One key does not mean one schema.</li>
<li>Not every route is OpenAI-compatible.</li>
<li>Model lists and account availability can change.</li>
<li>Route registration does not prove every account can use every model.</li>
<li>Price, credits, latency, throughput, and uptime need current first-party
checks.</li>
<li>Generated or parsed media still needs rights and output review.</li>
<li>A unified API does not guarantee lower cost or better reliability.</li>
</ul>
<h2>FAQ</h2>
<h3>What does “unified AI API” mean?</h3>
<p>It means that selected task families share an access layer, base URL,
authentication pattern, and route-discovery process. It should not mean that
all models use one body or response schema.</p>
<h3>Does every endpoint use one request format?</h3>
<p>No. Image, video, speech, chat, and commerce routes have separate contracts.
Validate only the fields documented for the selected model and route.</p>
<h3>Is every XPLA route OpenAI-compatible?</h3>
<p>No. The chat route follows the Chat Completions contract. Do not apply one
global compatibility assumption to image, video, speech, or commerce routes.</p>
<h3>How should an application handle video generation?</h3>
<p>Persist the task ID, poll at bounded intervals, stop on a terminal state, and
retrieve content through the documented authenticated route.</p>
<h3>Does a unified API guarantee lower cost or higher uptime?</h3>
<p>No. Those claims require current pricing and controlled operational evidence.
An access layer alone cannot prove them.</p>
<h2>Media plan</h2>
<p>This tutorial uses one original 1200×630 architecture diagram rather than a
fabricated product screenshot. The PNG has been losslessly optimized and
contains no customer data, credential, private URL, or generated UI claim.</p>
<h2>Internal links</h2>
<ul>
<li><a href="https://xplaai.com/en-us/api/">Unified AI API guide</a> — maintained route guide</li>
<li><a href="https://xplaai.com/en-us/api/ai-image/">AI image generation API</a> — image route details</li>
<li><a href="https://xplaai.com/en-us/api/ai-video/">AI video API</a> — asynchronous video lifecycle</li>
<li><a href="https://xplaai.com/en-us/skills/">Commerce Skills</a> — packaged workflow alternative</li>
</ul>
<h2>CTA</h2>
<p>Read the maintained
<a href="https://xplaai.com/en-us/api/">XPLA unified AI API guide</a> and choose the route
before creating one small contract test.</p>
<p>Disclosure: I prepared this tutorial for XPLA after rechecking the maintained
guide and route contracts on September 1, 2026. I have not included a paid
result, customer case, traffic claim, or ranking promise.</p>
]]></content:encoded></item></channel></rss>