SuperAPI AI Directory

starchild-auth

General & other

View repository

Updated 10-16 09:57

About this skill

🔑 Starchild Auth SDK — Complete Development Guide

Integrate Starchild OAuth login into any web application. The SDK handles OAuth popup flow, token refresh, unified StarchildAuthError, namespaced helpers (auth.chat / auth.credit …), and 60+ API methods for full Agent access.

Versioning Strategy

Artifact Current Version When to bump
npm starchild-auth-sdk 0.4.1 Code / public API changes
This Skill starchild-auth 1.11.0 Integration guide / scenario documentation changes (can be independent of the package)

Two semver tracks not bound to each other: changing only docs may bump only the skill; changing only the implementation must bump the package (the skill is usually bumped in minor/patch as well to describe new capabilities).


Architecture Overview

第三方网站 (your-app.com)
    │
    ├─ StarchildAuth SDK (starchild-auth-sdk)
    │   ├─ auth.login()       → 弹出 starchild-web 授权页面
    │   ├─ auth.logout()      → go-api POST /v1/oauth/logout
    │   ├─ auth.bindAccount() → 跳转主站 Linked accounts 绑定正式账号
    │   └─ token refresh      → go-api POST /v1/oauth/refresh
    │
    ├─ API 调用 (chat scope)
    │   ├─ chat/stream      → clawd (SSE 流式响应)
    │   ├─ /api/clawd/*     → ai-agent (线程/消息)
    │   ├─ /api/cloud/*     → ai-agent (容器管理)
    │   └─ WebSocket        → clawd (文件同步/终端/指标)
    │
    ├─ Credits API (credit:read / credit:write)
    │   └─ https://credit.iamstarchild.com
    │       ├─ GET  /api/credits|charges|topups|usage/daily|pending|tx/{hash}
    │       ├─ POST /api/stripe/create-session | gift-cards/redeem | points/exchange
    │       ├─ GET/POST /api/kyc/* | /api/referral/* | /api/migration/reward/*
    │       └─ GET  /api/public/users/{id}/woo-bonus
    │
    └─ 用户信息
        └─ /v1/oauth/userinfo → ai-agent

App Registration and Review

Registration Flow

  1. In iamstarchild.com → More → OAuth Apps → Create App
  2. fill in the information:
    • Name (required): App name
    • Allowed Origin (required): the third-party app's own page Origin(not starchild-web). Example: production https://your-app.com; local http://localhost:5173 / http://localhost:3333. Only origin (scheme://host[:port], no path/query/hash) is allowed; non-localhost must be https://. Multiple can be configured.Do nottreat http://localhost:6066 as a third-party origin——6066 is the main site web local port, already allowed in the server-side static CORS.
    • Scopes: Check the required permissions
    • System Prompt (optional): Custom Agent behavior
  3. Select only profile → auto-approved, get Client ID immediately
  4. Selected chat / credit:read / credit:write → entersadmin review, Client ID is generated only after approval

Scope Permission System

Scope Permission Scope Review
profile View username, avatar, ID Auto-approved
chat Agent conversation, thread management, container management, skills, media, scheduled tasks, wallet read, billing, WebSocket Requires review
credit:read View Credits balance and account status Requires review
credit:write Top up/purchase Credits, redeem Points (implies credit:read) Requires review

Note: Containerdeleteoperations are blocked for all OAuth tokens (returns 403). This is a server-side hard limit.


Installation

npm / yarn / pnpm

npm install starchild-auth-sdk
# or: yarn add starchild-auth-sdk
# or: pnpm add starchild-auth-sdk

CDN (plain HTML)

<!-- UMD build — use with plain <script> tags -->
<script src="https://unpkg.com/starchild-auth-sdk/dist/starchild-auth.umd.cjs"></script>

<!-- China mirror -->
<script src="https://registry.npmmirror.com/starchild-auth-sdk/latest/files/dist/starchild-auth.umd.cjs"></script>

UMD build exports window.StarchildAuth(the constructor itself, not a namespace). ESM build (starchild-auth.js) is used for <script type="module"> or bundlers.


Initialization and Login

import { StarchildAuth } from 'starchild-auth-sdk'

const auth = new StarchildAuth({
  clientId: 'your-client-id',          // 必填
  scope: 'profile chat credit:read credit:write', // 空格分隔;需要 Credits 时加上 credit scopes
  // clawdApiBase: 'https://preview.iamstarchild.com', // chat/stream HTTP
  // clawdWsBase: 'wss://preview.iamstarchild.com',   // /ws/sync|terminal|metrics
  // creditApiBase: 'https://credit.iamstarchild.com', // 可选,默认生产域名

  // 登录成功回调(popup 或 autoLogin 恢复 session 时触发)
  onLogin: ({ accessToken, refreshToken, expiresIn, userInfo }) => {
    console.log('Logged in:', userInfo.agentName, 'guest=', userInfo.isGuest)
    // userInfo = { userInfoId, agentName, agentAvatar, isGuest }
  },

  onLogout: () => { /* 清除本地状态 */ },
  onTokenRefresh: (newToken) => { /* 更新本地 token */ },
  onTokenRefreshFailed: () => { /* session 过期 */ },

  // 可选配置
  autoLogin: true,          // 默认 true — 从 localStorage 恢复 session
  origin: 'https://iamstarchild.com',  // Starchild 站点
  refreshInterval: 720000,  // 自动刷新间隔 (ms),默认 12 分钟
})

Token Lifecycle

  • Access Token: valid for 15 minutes, automatically refreshed every 12 minutes;auth.getToken()
  • Refresh Token: valid for 7 days, stored in localStorage 's starchild_rt_{clientId} key;auth.getRefreshToken() only exposes the same value in memory
  • autoLogin: automatically restores the session using the refresh token on page load
  • visibilitychange: automatically refreshes the token when switching back from the background

getRefreshToken() Security Model

  • Refresh token alreadywritten in the integrator origin's localStorage(required by autoLogin); the public getter does not expandthe threat surface, it is just a readable in-memory copy.
  • Preferletting the SDK refresh by itself:refreshToken() / scheduled auto-refresh / visibility refresh.
  • If you copy it to your own storage: treat it as a password—no logging, no sending to third-party backends, no putting it in URLs.
  • XSS on the integrator origin can already read localStorage; mitigate with CSP and by avoiding inline script.

Guest accounts and binding a permanent login method

There is no loginAsGuest(). Guest and permanent accounts go throughthe same auth.login() main-site popup flow; isGuest: trueis only obtained when the user selects continue-as-guest (or an equivalent entry point) on the main site. Third parties must not build their own guest login.

OAuth login may return a Guest (temporary) account(userInfo.isGuest === true). A Guest can use the authorized scopes normally, but has not bound a permanent login method (Google / X / Email / Phone / Wallet).

Binding must be completed on the Starchild main site, third parties must not build their own binding page. The SDK provides a redirect method:

// 登录后检查是否为 Guest
const user = auth.getUserInfo()
// 或刷新:const user = await auth.fetchUserInfo()

if (auth.isGuest() || user?.isGuest) {
  // 打开主站 Account management → Linked accounts
  // URL: https://iamstarchild.com/?account_tab=linked-accounts
  const win = auth.bindAccount()
  if (!win) {
    // 弹窗/新标签被拦截时,可自行跳转
    window.location.href = auth.getBindAccountUrl()
  }
}

// 仅需要 URL(例如自己渲染按钮 href)
const bindUrl = auth.getBindAccountUrl()
// => `${origin}/?account_tab=linked-accounts`
Method Returns Description
isGuest() boolean Whether the current user is a Guest; if not logged in, false
getBindAccountUrl() string Main-site binding page URL (Linked accounts tab)
bindAccount() Window | null Open the main-site binding flow in a new tab; if blocked, returns null

After the main site opens, based on ?account_tab=linked-accounts it will automatically open account management and switch to Linked accounts. After the user completes binding, the third-party app should see it after the next fetchUserInfo() / token refresh. isGuest: false。


Core API call patterns

Request format

All SDK methods handle token injection automatically. When sending requests manually:

const headers = {
  'Authorization': `Bearer ${accessToken}`,
  'Content-Type': 'application/json',
}

clawd endpoints must include fly-force-instance-id:clawd(preview.iamstarchild.com) Each Fly Machine is a single-user container; the container ownership (IDOR) check requires the request to land on the current user's container, otherwise it returns 403 “Access denied: you do not own this resource”. The OAuth access token does not contain containerId; the SDK automatically resolves it via GET /api/cloud/containers and injects the fly-force-instance-id: <container_id> header; when manually testing clawd endpoints with curl, you must explicitly include this header, otherwise you will get a 403.

Endpoint address:

  • ai-agent REST API: https://ai-api.iamstarchild.com(threads/messages/containers/skills, etc.)
  • clawd HTTP API: https://preview.iamstarchild.com(chat/stream、scheduled-jobs、models)
  • Token endpoint: https://go-api.iamstarchild.com/v1(go-api)

Namespace API (compatibility layer)

All flat methods are preserved. Namespaces are plan 303-style grouping aliases, and the two are equivalent:

await auth.sendMessage('hi')
await auth.chat.send('hi')          // alias

await auth.getCredits()
await auth.credit.getBalance()      // alias

await auth.listThreads()
await auth.threads.list()
Namespace Main methods
auth.profile fetchUserInfo, getUserInfo, isGuest, bindAccount, getBindAccountUrl
auth.chat send/sendMessage, reconnect/reconnectStream, cancelRun, getModel/setModel, WS factories
auth.threads create, list, get, delete, search, pin, updateTitle
auth.messages list, delete
auth.containers list, status, metrics, deploy, start, stop, restart, wake, rename, delete, …
auth.skills catalog, search, detail
auth.media uploadImage, transcribeAudio, synthesizeSpeech
auth.shares create, list, get, delete, fork
auth.feedback rate, delete
auth.jobs list, create, get, pause, resume, restart
auth.wallet getPortfolio, list, create, delete, exportPrivateKey, createOnrampSession
auth.credit Balance/transactions/Stripe/gift cards/Points/KYC/Referral/migration/WOO (see Scenario 8)

Points redemption, KYC, Referral are now open to OAuth(requires credit:read / credit:write), not main-site exclusive.


Scenario 1: Send a message and read the SSE stream response

This is the most core interaction pattern. Messages are returned as a stream via SSE (Server-Sent Events).

SDK method

const stream: Response = await auth.sendMessage('Hello, analyze this data')

// SSE 是流式响应,需要逐块读取
const reader = stream.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''

while (true) {
  const { done, value } = await reader.read()
  if (done) break

  buffer += decoder.decode(value, { stream: true })
  const lines = buffer.split('\n')
  buffer = lines.pop() || ''  // 保留未完成的行

  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const event = JSON.parse(line.slice(6))
      // event.type 决定处理方式
      handleStreamEvent(event)
    }
  }
}

Native fetch method (without using the SDK)

// POST /chat/stream — SSE 流式聊天(clawd 端点)
const response = await fetch('https://preview.iamstarchild.com/chat/stream', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    message: 'Hello, analyze this data',
    thread_id: threadId,    // 可选,不传则创建新 thread
  }),
})

// 读取 SSE 流(同上)

SSE event types

Event Type Meaning Key fields
agent_start Agent starts processing session_key — used for subsequent reconnects
text_delta Text incremental output text — newly generated text fragment
tool_use Tool call tool_name, tool_input
tool_output Tool result tool_output — tool output
agent_end Agent complete stop_reason — end_turn / tool_use
error Error message — error description
function handleStreamEvent(event: any) {
  switch (event.type) {
    case 'agent_start':
      console.log('Agent started, session:', event.session_key)
      // 保存 session_key 用于断线重连
      break
    case 'text_delta':
      process.stdout.write(event.text)  // 实时输出
      break
    case 'tool_use':
      console.log(`Using tool: ${event.tool_name}(${event.tool_input})`)
      break
    case 'tool_output':
      console.log('Tool result:', event.tool_output)
      break
    case 'agent_end':
      console.log('Done, reason:', event.stop_reason)
      break
    case 'error':
      console.error('Stream error:', event.message)
      break
  }
}

Reconnect to the SSE stream

When the SSE connection drops (network issues, page switching, etc.), use session_key to reconnect:

// POST /chat/stream/reconnect?session_key=xxx&channel=web
const stream = await auth.reconnectStream(sessionKey)
// 读取方式同 sendMessage

Scenario 2: Managing Conversation Threads

// 创建线程
const thread = await auth.createThread('My analysis')
// thread = { id: string, title: string, created_at: string, ... }

// 列出所有线程
const { threads } = await auth.listThreads()

// 获取线程消息
const { messages } = await auth.listMessages(thread.id, 50)  // 最近 50 条
// messages[0] = { id, role: 'user'|'assistant', content: [...], created_at }

// 搜索线程
const result = await auth.searchThreads('analysis')

// 删除线程
await auth.deleteThread(thread.id)

// 删除消息
await auth.deleteMessages(thread.id)

Scenario 3: Managing Containers

Containers are Fly.io virtual machines that run the Agent.

// 部署新容器
const container = await auth.deployContainer()
// container = { container_id, name, state, region, ... }

// 列出所有容器
const { containers } = await auth.listContainers()

// 获取容器状态
const status = await auth.getContainerStatus(container.container_id)
// status = { state: 'started'|'stopped'|'suspended'|..., ... }

// 启动/停止/重启
await auth.startContainer(container_id)
await auth.stopContainer(container_id)
await auth.restartContainer(container_id)

// 重命名
await auth.renameContainer(container_id, 'production-agent')

// 获取指标
const metrics = await auth.getContainerMetrics(container_id)
// metrics = { cpu: { series: [...] }, memory: { series: [...] }, disk: { series: [...] } }

// ⚠️ 删除容器 — OAuth token 无法执行(服务端返回 403)
// await auth.deleteContainer(container_id)  // 总是失败

Scenario 4: WebSocket Connection

Real-time metrics (CPU/memory/disk)

const ws = auth.createMetricsWebSocket()

ws.onopen = () => console.log('Metrics connected')
ws.onmessage = (e) => {
  const { cpu_percent, memory_used_bytes, memory_total_bytes, disk_used_bytes } = JSON.parse(e.data)
  console.log(`CPU: ${cpu_percent}%, Mem: ${memory_used_bytes}/${memory_total_bytes}`)
}
ws.onclose = () => console.log('Disconnected — implement reconnection logic')

File sync

const ws = auth.createSyncWebSocket()

ws.onopen = () => {
  // 订阅文件变更
  ws.send(JSON.stringify({
    type: 'sync:subscribe',
    payload: { paths: ['/src'] }
  }))
}

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  switch (msg.type) {
    case 'sync:connected':
      console.log('Sync ready, session:', msg.payload.sessionId)
      break
    case 'file:created':
      console.log('New file:', msg.payload.path)
      break
    case 'file:updated':
      // msg.payload = { path, type: 'file'|'directory', triggeredBy: 'watcher'|'agent'|'user' }
      console.log('Changed:', msg.payload.path, 'by', msg.payload.triggeredBy)
      break
    case 'file:deleted':
      console.log('Deleted:', msg.payload.path)
      break
    case 'file:moved':
      console.log('Moved:', msg.payload.oldPath, '→', msg.payload.newPath)
      break
  }
}

Terminal

// sessionId 从 SSE chat stream 的 terminal:connected 事件获取
const ws = auth.createTerminalWebSocket(sessionId)

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  switch (msg.type) {
    case 'connected':
      console.log('Terminal ready, session:', msg.sessionId)
      break
    case 'output':
      process.stdout.write(msg.data)
      break
    case 'error':
      console.error('Terminal error:', msg.message)
      break
  }
}

// 发送命令
ws.send(JSON.stringify({ type: 'input', data: 'ls -la\n' }))
// 调整终端大小
ws.send(JSON.stringify({ type: 'resize', cols: 120, rows: 40 }))

WebSocket reconnection

Browser WebSocket does not support automatic reconnection; you need to implement it yourself:

class WSReconnect {
  private ws: WebSocket | null = null
  private attempts = 0
  private maxAttempts = 5

  connect(factory: () => WebSocket) {
    this.ws = factory()
    this.ws.onclose = () => {
      if (this.attempts < this.maxAttempts) {
        const delay = Math.min(1000 * 2 ** this.attempts, 30000)
        setTimeout(() => {
          this.attempts++
          this.connect(factory)
        }, delay)
      }
    }
    this.ws.onopen = () => { this.attempts = 0 }
  }
}

Scenario 5: Skills and Media

// 浏览技能目录
const catalog = await auth.getSkillsCatalog()
// catalog = { official: [{ source, name, description, ... }], community: [...], installed: [...] }

// 搜索技能
const results = await auth.searchSkills('trading')

// 获取技能详情
const detail = await auth.getSkillDetail('official', 'orderly-trading')

// 上传图片
const image = await auth.uploadImage(base64data, 'image/png')
// image = { url: string, filename: string }

// 语音转文字
const text = await auth.transcribeAudio(audioBase64)
// text = { text: string }

// 文字转语音
const audio = await auth.synthesizeSpeech('Hello world')
// audio = { audio_base64: string, format: 'mp3' }

Scenario 6: Sharing and Feedback

// 创建对话分享
const share = await auth.createShare(threadId)
// share = { share_id: string, share_url: string }

const share = await auth.createShare(threadId, ['msg-1', 'msg-2'])  // 指定消息

// 列出分享
const { shares } = await auth.listShares()

// 删除分享
await auth.deleteShare(shareId)

// 复制分享
await auth.forkShare(shareId)

// 点赞/踩消息
await auth.rateMessage(messageId, 'like')
await auth.rateMessage(messageId, 'dislike', 'Not accurate')

// 取消反馈
await auth.deleteFeedback(messageId)

Scenario 7: Wallet and Billing

// 获取投资组合
const portfolio = await auth.getPortfolio()
// portfolio = { total_value_usd, tokens: [...] }

// 列出钱包
const { wallets } = await auth.listWallets()

// 创建/删除钱包
const wallet = await auth.createWallet()
await auth.deleteWallet(walletAddress)

// Coinbase Onramp
const session = await auth.createOnrampSession({
  amount: '100',  // USD
  currency: 'USD',
})
// session = { url: string } — redirect user to this URL

Scenario 8: Credits (balance / top-up / Points / KYC / Referral)

Service: starchild-credit-api
Base: creditApiBase, default https://credit.iamstarchild.com
auth: Authorization: Bearer <oauth_access_token>
Scope:

  • credit:read — all GET (balance, transactions, pending, tx, points balance, KYC status, referral query, migration status)
  • credit:write — write operations (Stripe session, gift card, points redemption, KYC write, referral bind, migration claim);implicit read

When registering an OAuth App, you need to check the corresponding scopes; the token will only carry them after approval.

Open scope: balance/transactions, Stripe, gift card,Points redemption、KYC、Referral, migration reward, WOO bonus (public) are all open to OAuth; namespace syntax:auth.credit.*。

Initialization

const auth = new StarchildAuth({
  clientId: 'your-client-id',
  scope: 'profile chat credit:read credit:write',
  // creditApiBase: 'https://credit.iamstarchild.com', // 默认值,本地可改
  onLogin: ({ userInfo }) => console.log(userInfo),
})

Balance and transactions (credit:read)

// 当前余额
const bal = await auth.getCredits()
// bal.credit_balance — 可用余额
// bal.pending_credit — 待入账
// bal.total_recharged / bal.total_used
// bal.daily_balance — 订阅日额度(若有)
// bal.container_id / bal.user_id

// 扣费记录(默认近 24h,可分页 + 时间窗)
const charges = await auth.getCreditCharges({
  page: 1,
  page_size: 20,
  start_time: '2026-01-01T00:00:00Z', // 可选 ISO8601
  end_time: '2026-01-31T23:59:59Z',
})
// charges.charges[]: { amount, api_type, balance_after, description, created_at, ... }
// charges.pagination: { page, page_size, has_more }
// charges.time_range: { start_time, end_time }

// 充值记录
const topups = await auth.getCreditTopups({ page: 1, page_size: 20 })
// topups.topups[]: { amount, chain, tx_hash, balance_after, created_at, ... }

// 每日用量
const usage = await auth.getCreditDailyUsage({ days: 7 })
// usage.daily[] / usage.by_api[]

// 待入账
const pending = await auth.getPendingCredit()
// pending.status === 'no_pending' | 'pending_sync' | (有 machine 时直接带 pending_credit)

// 轮询链上/支付 tx
const tx = await auth.getCreditTxStatus(txHash)
// 1) status==='not_detected' && !credited && !pending → 继续轮询
// 2) pending && !container_id → 已进 pending_credit,可停
// 3) pending && container_id → 等 flush,继续轮询
// 4) credited === true → 已入账,停

Field quick reference:CreditBalance(GET /api/credits)

Field Type Description
user_id string? User ID
container_id string Associated container (may be empty)
credit_balance number Available Credits
daily_balance number? Remaining daily subscription quota
total_recharged number Cumulative top-up
total_used number Cumulative consumption
pending_credit number? Pending credit
ipv6 / name / is_active / status / hint optional Machine/status information

Stripe top-up (credit:write)

const { url } = await auth.createStripeSession({
  amount_usd: 20,
  success_url: 'https://your-app.com/billing?ok=1',
  cancel_url: 'https://your-app.com/billing?cancel=1',
})
window.location.href = url
// 支付完成后可用 getCreditTxStatus / getCredits / getPendingCredit 确认到账
Request field Type Description
amount_usd number USD amount, e.g. 10 = $10
success_url string Absolute URL to redirect back to on successful payment
cancel_url string Absolute URL to redirect back to on cancellation

Response:{ url: string } — Stripe Checkout URL.

Gift card (credit:write)

const r = await auth.redeemGiftCard('GIFT-CODE-XXX')
// 或 auth.redeemGiftCard({ code: 'GIFT-CODE-XXX' })
// r.amount, r.credited_to: 'machine' | 'pending'
// r.new_balance / r.pending_credit

Points redemption for Credits (read to check balance / write to redeem)

const pts = await auth.getPointsExchangeBalance()
// pts.available_points, pts.exchange_rate, pts.exchanged_credits, ...

const ex = await auth.exchangePoints(
  { points: 1000 },
  crypto.randomUUID(), // 推荐传 Idempotency-Key,防重试双花
)
// 也可 auth.exchangePoints(1000, idemKey)
// ex.credits_received, ex.new_credit_balance, ex.idempotent?

KYC (may be required for large points redemptions)

const kyc = await auth.getKycStatus()
// kyc.verified / kyc.exempt / kyc.exchanged_credits / kyc.threshold_credits

if (!kyc.verified && !kyc.exempt) {
  const intent = await auth.createKycSetupIntent()
  // intent.client_secret → 交给 Stripe.js / Payment Element 完成绑卡
  // 成功后:
  await auth.verifyKyc(intent.setup_intent_id)
}

Referral

const ref = await auth.getReferralStatus()
// ref.my_referral_code, ref.can_bind, ref.has_bound_inviter, ref.invited_by

if (ref.can_bind) {
  await auth.bindReferralCode('INVITE-CODE') // credit:write,仅一次
}

const invitees = await auth.getReferralInvitees()
// invitees.invitees[], invitees.total_bonus_earned, ...

Migration reward

const st = await auth.getMigrationRewardStatus()
if (st.eligible && !st.already_claimed) {
  const claim = await auth.claimMigrationReward() // credit:write
  // claim.amount, claim.credited_to, claim.new_balance
}

WOO Staking Bonus (public API)

const woo = await auth.getWooBonus() // 默认当前登录 userInfoId
// woo.bonus_percent, woo.max_staked_woo, woo.matched_wallet

Field quick reference: other commonly used responses

CreditChargeItem / getCreditCharges

Field Type Description
amount number Credits charged
api_type string Billing API category
balance_after number Balance after charge
description string Description
created_at string ISO8601
machine_ipv6 string Machine IPv6
call_type / agent_id string? Optional call metadata
pagination.has_more boolean Whether there is a next page

CreditTopupItem / getCreditTopups

Field Type Description
amount number Top-up amount
chain string Chain / channel (including stripe)
tx_hash string Transaction hash
balance_after number Balance after crediting
created_at string ISO8601

CreditTxStatus / getCreditTxStatus

Field Type Description
credited boolean Whether already credited
pending boolean Whether processing
status 'not_detected'? No on-chain tx detected
balance_after number? Balance after crediting
amount / chain / tx_hash optional Details after detection
container_id string? Empty string indicates no container, enter pending

RedeemGiftCardResponse

Field Type Description
code string Gift card code
amount number Credits received
credited_to 'machine' | 'pending' Crediting target
new_balance / pending_credit number | null Corresponding balance

PointsExchangeBalance / PointsExchangeResponse

Field Type Description
available_points number Redeemable points
exchange_rate string Exchange rate text
points_spent number Points consumed this time
credits_received number Credits obtained this time
new_credit_balance number Balance after redemption
idempotent boolean? Idempotent replay

KycStatus / KycVerifyResponse

Field Type Description
verified boolean Whether KYC is done
exempt boolean Whether exempt
exchanged_credits / threshold_credits number Redeemed / threshold
card_last4 / card_brand string Card verification result

ReferralStatus / ReferralInviteesResponse

Field Type Description
my_referral_code string | null Own referral code
can_bind boolean Whether a referrer can still be bound
invitee_count number Number of referrals
total_bonus_earned number Cumulative referral bonus

MigrationRewardClaimResponse

Field Type Description
amount number Reward Credits
credited_to 'machine' | 'pending' | 'none' Crediting location
already_claimed boolean Whether already claimed
message string Status description

WooBonusResponse

Field Type Description
bonus_percent number Extra credit percentage
max_staked_woo number Maximum staked WOO
matched_wallet string Wallets hitting the tier
wallets_checked[] { wallet, staked_woo } Check details

See the SDK for the complete TypeScript definitions and field JSDoc:starchild-auth-sdk/src/types.ts(after build dist/index.d.ts)。

When Scope is insufficient

The server returns 403, and the body is similar to:

{ "detail": "Insufficient scope: credit:read or credit:write is required." }

When a write endpoint is missing credit:write :

{ "detail": "Insufficient scope: credit:write is required for this operation." }

Integrators should guide users to re- login() and apply for complete credit scopes, or check the box in the OAuth App console and re-authorize.


Scenario Nine: Message Queuing and Injection (Sending New Messages While the Agent Is Running)

When the Agent is processing a message (the SSE stream has not ended), the user may send a new message. At this point you cannot directly call /chat/stream, but instead add the message to thequeue。

Frontend implementation pattern (refer to starchild-web)

// 1. 检查 Agent 是否正在运行
const isAgentActive = isStreaming || !!agentBackgroundRunning[threadId]

if (isAgentActive) {
  // 2. 消息加入本地队列(不立即发送到后端)
  const queuedId = `queued-${Date.now()}`
  dispatch(addToMessageQueue({
    threadId,
    message: {
      id: queuedId,
      content: message,
      images: images,      // 可选:base64 图片
      files: files,        // 可选:已上传文件引用
      quote: quoteOptions, // 可选:引用的消息
      status: 'pending',   // pending → sending → sent
      createdAt: Date.now(),
    },
  }))

  // 3. 显示 "当前消息已排队,Agent 完成后自动发送" 提示
  dispatch(updateQueuedMessageStatus({ threadId, messageId: queuedId, status: 'sending' }))

  // 4. 后端在 /chat/stream 完成后,检查 messageQueue
  //    取出 FIFO 的第一条,调用下一个 /chat/stream
} else {
  // Agent 空闲,直接发送
  await sendToChatStream(message, images, files)
}

/chat/stream request body format (complete)

{
  "message": "Hello, analyze this data",
  "thread_id": "thread-uuid",
  "channel": "web",
  "message_id": "queued-1234567890",
  "model": "claude-3-5-sonnet-20241022",
  "images": [
    {
      "base64_data": "...",
      "media_type": "image/png"
    }
  ],
  "files": [
    {
      "name": "data.csv",
      "workspace_path": "/workspace/data.csv",
      "mime_type": "text/csv",
      "size": 1024
    }
  ],
  "quote": {
    "source_message_id": "msg-abc123",
    "quoted_text": "The original message text...",
    "source_role": "user"
  }
}

/chat/stream response flow

POST /chat/stream → SSE 连接建立
  ← event: agent_start    { session_key: "sess-xxx" }
  ← event: text_delta     { text: "I'll analyze..." }
  ← event: tool_use       { tool_name: "read_file", tool_input: {...} }
  ← event: tool_output    { tool_output: "file content..." }
  ← event: text_delta     { text: "Based on the data..." }
  ← event: agent_end      { stop_reason: "end_turn" }
SSE 连接关闭
→ 后端检查 messageQueue[threadId]
→ 如果有排队消息 → 自动开始下一个 /chat/stream

Detailed explanation of SSE event types

Event Meaning payload example
agent_start The Agent starts processing, returns session_key for reconnection {session_key: "sess-abc123"}
text_delta Incremental text output (token by token) {text: "Hello"}
tool_use The Agent calls a tool {tool_name: "read_file", tool_input: {path: "/a.txt"}}
tool_output The tool returns a result {tool_output: "file contents..."}
agent_end The Agent completes a round of conversation {stop_reason: "end_turn" | "tool_use"}
error Stream error {message: "Error description"}
agent:interrupted The Agent was interrupted (user canceled/timeout) {reason: "..."}

/chat/runs/cancel cancels a run

// POST /chat/runs/cancel?thread_id=xxx
await auth.cancelRun(threadId)
// 这会中断当前正在运行的 SSE 流
// SSE 连接会收到 agent_end 或 agent:interrupted 事件后关闭

/chat/stream/reconnect reconnects after disconnection

When the SSE connection disconnects unexpectedly (network issue, page switch), use session_key to reconnect:

// POST /chat/stream/reconnect?session_key=sess-xxx&channel=web
const stream = await auth.reconnectStream(sessionKey)

// reconnect 返回的 SSE 事件格式相同
// 它会从中断点继续推送剩余的事件
// 如果 Agent 已完成,会立即收到 agent_end

Scenario 10: Agent Integration (Direct Token Injection)

Agents can use the SDK without a browser popup:

import { StarchildAuth } from 'starchild-auth-sdk'

// Agent 从环境变量或 OAuth 回调获取 token
const accessToken = process.env.STARCHILD_TOKEN!

const auth = new StarchildAuth({
  clientId: process.env.CLIENT_ID!,
  scope: 'profile chat',
  autoLogin: false,  // 不弹出浏览器窗口
  onLogin: () => {},
})

// 注入 token(绕过 popup 流程)
;(auth as any)._accessToken = accessToken

// 现在可以调用所有方法
const threads = await auth.listThreads()
const message = await auth.sendMessage('Summarize my threads')

Scenario 11: Recommended Chat UI Component Integration

The Auth SDK is a pure API layer (no UI components). Below are two recommended open-source React Chat component libraries, along with complete adapter code for interfacing with the SDK SSE stream, to help third-party applications quickly build a ChatGPT-style conversation interface.

Official documentation:

Selection Comparison

Dimension assistant-ui MUI X Chat
Package Name @assistant-ui/react @mui/x-chat
Integration Difficulty Low — a single async *run() generator is enough Medium — requires converting SSE to ReadableStream<ChatMessageChunk>
UI Dependencies Framework-agnostic (shadcn / Tailwind style, no theme binding) Strongly bound to MUI Material theme(brings in @mui/material + @emotion/*)
React Native Supported (@assistant-ui/react-native) Not supported
Streaming Protocol yield accumulated content (replaces the previous one each time) chunk protocol (start → text-delta → finish)
Tool Calling yield { type: 'tool-call', ... } intuitive tool-call-* chunk sequence
Multi-session RemoteThreadListAdapter or AssistantCloud listConversations + built-in sidebar
License MIT MIT (all features free, no Pro/Premium)
Community Size 1.4M+ weekly downloads, YC-backed, used by Anthropic / LangChain Relatively new, backed by the MUI ecosystem
Recommended First choice — suitable for most third-party applications Alternative — only recommended for projects already using MUI Material

Shared: SSE Stream Parsing helper

Both adapters need to parse the SDK sendMessage() returned SSE Response. Extract it as a shared function:

// lib/starchild-sse.ts
import type { SSEEvent } from 'starchild-auth-sdk'

/**
 * Parse Starchild SSE Response into an async iterable of events.
 * Stops early if the abort signal fires.
 *
 * SDK sendMessage() returns a raw Response whose body is an SSE stream.
 * Each line starting with "data: " contains a JSON event:
 *   agent_start | text_delta | tool_use | tool_output | agent_end | error
 */
export async function* parseStarchildSSE(
  response: Response,
  signal?: AbortSignal,
): AsyncGenerator<SSEEvent> {
  if (!response.ok) {
    const body = await response.text().catch(() => '')
    throw new Error(`Chat API ${response.status}: ${body.slice(0, 200)}`)
  }
  const reader = response.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ''

  while (true) {
    if (signal?.aborted) {
      reader.cancel()
      break
    }
    const { done, value } = await reader.read()
    if (done) break

    buffer += decoder.decode(value, { stream: true })
    const lines = buffer.split('\n')
    buffer = lines.pop() || ''

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue
      try {
        yield JSON.parse(line.slice(6)) as SSEEvent
      } catch {
        // Skip malformed JSON lines
      }
    }
  }
}

Option A: assistant-ui (Recommended)

Install

# 推荐使用 pnpm(更快、磁盘占用更小);yarn / bun 亦可
pnpm add @assistant-ui/react
# 或: yarn add @assistant-ui/react
# 或: npm install @assistant-ui/react

# 脚手架生成 Thread 组件(shadcn / Tailwind 风格)
npx assistant-ui init
npx assistant-ui add thread

ChatModelAdapter Implementation

assistant-ui's LocalRuntime only requires implementing a ChatModelAdapter.run() function (async * generator). In the loop, yield accumulated content(replaces the previous one each time, not a delta):

// runtime/starchild-adapter.ts
import type { ChatModelAdapter } from '@assistant-ui/react'
import { StarchildAuth } from 'starchild-auth-sdk'
import { parseStarchildSSE } from '@/lib/starchild-sse'

/**
 * Create an assistant-ui ChatModelAdapter backed by Starchild Auth SDK.
 * The auth instance is initialized in the Provider and passed in.
 */
export function createStarchildAdapter(auth: StarchildAuth): ChatModelAdapter {
  return {
    async *run({ messages, abortSignal, unstable_threadId }) {
      // 1. Extract last user message text
      const lastUser = [...messages].reverse().find(m => m.role === 'user')
      const text = lastUser?.content
        .filter(c => c.type === 'text')
        .map(c => c.text)
        .join('\n') ?? ''

      if (!text) {
        yield { content: [{ type: 'text', text: '' }] }
        return
      }

      // 2. Call SDK sendMessage — returns SSE Response
      //    threadId: continue existing thread, or omit to create new
      const response = await auth.sendMessage(text, {
        threadId: unstable_threadId,
      })

      // 3. Parse SSE stream and yield cumulative content
      let fullText = ''
      const toolCalls = new Map<string, any>()

      for await (const event of parseStarchildSSE(response, abortSignal)) {
        switch (event.type) {
          case 'agent_start':
            // event.session_key — save for reconnect if needed
            break

          case 'text_delta':
            fullText += event.text
            yield {
              content: [
                ...(fullText ? [{ type: 'text' as const, text: fullText }] : []),
                ...Array.from(toolCalls.values()),
              ],
            }
            break

          case 'tool_use': {
            // Accumulate tool calls outside the loop (per assistant-ui best practice)
            const id = event.tool_use_id || crypto.randomUUID()
            toolCalls.set(id, {
              type: 'tool-call' as const,
              toolCallId: id,
              toolName: event.tool_name,
              args: event.tool_input,
              argsText: JSON.stringify(event.tool_input),
            })
            yield {
              content: [
                ...(fullText ? [{ type: 'text' as const, text: fullText }] : []),
                ...Array.from(toolCalls.values()),
              ],
            }
            break
          }

          case 'tool_output': {
            // Update the matching tool call with its result.
            // IMPORTANT: create a new object (not mutate) so React detects the change.
            const id = event.tool_use_id
            if (id && toolCalls.has(id)) {
              const existing = toolCalls.get(id)
              toolCalls.set(id, { ...existing, result: event.tool_output })
              yield {
                content: [
                  ...(fullText ? [{ type: 'text' as const, text: fullText }] : []),
                  ...Array.from(toolCalls.values()),
                ],
              }
            }
            break
          }

          case 'agent_end':
            // Stream complete — final yield already done above
            break

          case 'error':
            throw new Error(event.message)

          case 'agent:interrupted':
            // User cancelled or timeout — stop yielding
            break
        }
      }

      // Ensure at least one yield with content
      if (!fullText && toolCalls.size === 0) {
        yield { content: [{ type: 'text', text: '' }] }
      }
    },
  }
}

RuntimeProvider Assembly

// runtime/StarchildRuntimeProvider.tsx
'use client'
import type { ReactNode } from 'react'
import {
  AssistantRuntimeProvider,
  useLocalRuntime,
} from '@assistant-ui/react'
import { StarchildAuth } from 'starchild-auth-sdk'
import { createStarchildAdapter } from './starchild-adapter'

// Singleton auth instance — SDK handles token refresh internally
const auth = new StarchildAuth({
  clientId: process.env.NEXT_PUBLIC_STARCHILD_CLIENT_ID!,
  scope: 'profile chat',
  onLogin: ({ userInfo }) => console.log('Logged in:', userInfo.agentName),
  onTokenRefreshFailed: () => {
    // Session expired — redirect to login or show login button
    window.location.reload()
  },
})

export function StarchildRuntimeProvider({
  children,
}: Readonly<{ children: ReactNode }>) {
  const runtime = useLocalRuntime(createStarchildAdapter(auth))
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {children}
    </AssistantRuntimeProvider>
  )
}

Multi-thread List (Optional)

If you need a left-side thread list (similar to the ChatGPT sidebar), implement RemoteThreadListAdapter and pass it to useLocalRuntime 's adapters.threadList。

⚠️ Interface Verification Reminder:RemoteThreadListAdapter 's method signatures may change with assistant-ui versions. The following examples are based on common patterns from public documentation;be sure to consult the latest documentation before integration:https://www.assistant-ui.com/docs/runtimes/concepts/threads

import { useLocalRuntime } from '@assistant-ui/react'
import type { RemoteThreadListAdapter } from '@assistant-ui/react'

// Thread list adapter — bridges SDK thread API to assistant-ui sidebar
const threadListAdapter: RemoteThreadListAdapter = {
  // Called on mount — load thread list from SDK
  async getThreads() {
    const { threads } = await auth.listThreads()
    return threads.map(t => ({
      id: t.thread_id,
      title: t.title || 'New Chat',
      createdAt: t.created_at ? new Date(t.created_at) : new Date(),
    }))
  },

  // Called when user clicks a thread in the sidebar
  async switchToThread(threadId: string) {
    // assistant-ui will call this to switch the active thread.
    // Messages are loaded separately via the adapter's run() or initialMessages.
    // If you need to pre-load history, use auth.listMessages(threadId)
    // and pass them as initialMessages to the runtime.
  },

  // Called when user creates a new thread
  async createThread() {
    const thread = await auth.createThread()
    return { id: thread.thread_id }
  },

  // Called when user deletes a thread
  async deleteThread(threadId: string) {
    await auth.deleteThread(threadId)
  },
}

// In provider:
const runtime = useLocalRuntime(createStarchildAdapter(auth), {
  adapters: { threadList: threadListAdapter },
})

Using in the Page

// app/chat/page.tsx
import { Thread } from '@/components/assistant-ui/thread'
import { StarchildRuntimeProvider } from '@/runtime/StarchildRuntimeProvider'

export default function ChatPage() {
  return (
    <StarchildRuntimeProvider>
      <Thread />
    </StarchildRuntimeProvider>
  )
}

Option B: MUI X Chat

Only recommended for projects already using the MUI Material theme.@mui/x-chat will forcibly bring in the @mui/material + @emotion/* dependency tree, making the cost of adoption high for non-MUI projects.

Install

# 推荐使用 pnpm(更快、磁盘占用更小);yarn / bun 亦可
pnpm add @mui/x-chat @mui/material @emotion/react @emotion/styled
# 或: yarn add @mui/x-chat @mui/material @emotion/react @emotion/styled
# 或: npm install @mui/x-chat @mui/material @emotion/react @emotion/styled

ChatAdapter Implementation

MUI X Chat's ChatAdapter.sendMessage() must return Promise<ReadableStream<ChatMessageChunk>>. You need to convert the SDK's SSE stream into MUI's chunk protocol (start → text-start → text-delta → text-end → finish):

// adapters/starchild-mui-adapter.ts
import type { ChatAdapter, ChatMessageChunk } from '@mui/x-chat/headless'
import { StarchildAuth } from 'starchild-auth-sdk'
import { parseStarchildSSE } from '@/lib/starchild-sse'

// Module-level state for reconnect / cancel
let lastSessionKey = ''
let currentThreadId = ''

export function createStarchildMuiAdapter(auth: StarchildAuth): ChatAdapter {
  return {
    async sendMessage({ message, signal }) {
      // Extract text from user message.
      // ChatMessage.content type varies by MUI version — handle both
      // string and structured content arrays defensively.
      const text = typeof message.content === 'string'
        ? message.content
        : Array.isArray(message.content)
            ? (message.content as Array<{ type: string; text?: string }>)
                .filter(c => c.type === 'text' && c.text)
                .map(c => c.text!)
                .join('\n')
            : String(message.content ?? '')

      // Call SDK sendMessage — returns SSE Response
      const response = await auth.sendMessage(text, {
        threadId: currentThreadId || undefined,
      })

      const messageId = crypto.randomUUID()
      const textId = crypto.randomUUID()

      // Transform SSE → MUI ReadableStream<ChatMessageChunk>
      return new ReadableStream<ChatMessageChunk>({
        async start(controller) {
          controller.enqueue({ type: 'start', messageId })

          let textStarted = false
          try {
            for await (const event of parseStarchildSSE(response, signal)) {
              switch (event.type) {
                case 'agent_start':
                  lastSessionKey = event.session_key
                  if (event.thread_id) currentThreadId = event.thread_id
                  break

                case 'text_delta':
                  if (!textStarted) {
                    controller.enqueue({ type: 'text-start', id: textId })
                    textStarted = true
                  }
                  controller.enqueue({
                    type: 'text-delta',
                    id: textId,
                    delta: event.text,
                  })
                  break

                case 'tool_use': {
                  // ⚠️ Tool-call chunk types are MUI-version-specific.
                  // The names below follow the MUI X Chat streaming protocol
                  // convention but may differ — always verify against:
                  // https://mui.com/x/react-chat/behavior/streaming/
                  const toolId = event.tool_use_id || crypto.randomUUID()
                  controller.enqueue({
                    type: 'tool-call-start',
                    id: toolId,
                    toolName: event.tool_name,
                  } as ChatMessageChunk)
                  controller.enqueue({
                    type: 'tool-call-input-available',
                    id: toolId,
                    input: JSON.stringify(event.tool_input),
                  } as ChatMessageChunk)
                  break
                }

                case 'tool_output':
                  // ⚠️ Same caveat as tool_use — verify chunk type name.
                  controller.enqueue({
                    type: 'tool-result',
                    id: event.tool_use_id || '',
                    result: event.tool_output,
                  } as ChatMessageChunk)
                  break

                case 'agent_end':
                  if (textStarted) {
                    controller.enqueue({ type: 'text-end', id: textId })
                    textStarted = false
                  }
                  break

                case 'error':
                  // Error chunks: the runtime wraps thrown errors into
                  // ChatError automatically. Throwing here is also valid
                  // and may be cleaner — see MUI error handling docs.
                  controller.enqueue({
                    type: 'error',
                    message: event.message,
                  } as ChatMessageChunk)
                  break
              }
            }
            // Close any open text stream
            if (textStarted) {
              controller.enqueue({ type: 'text-end', id: textId })
            }
            controller.enqueue({ type: 'finish', messageId })
          } catch (e) {
            // On error/abort, emit abort chunk and let runtime handle cleanup.
            // The 'abort' chunk type ends the stream per MUI protocol.
            controller.enqueue({ type: 'abort', messageId })
          } finally {
            controller.close()
          }
        },
      })
    },

    // Optional: conversation list — powers the built-in sidebar
    async listConversations() {
      const { threads } = await auth.listThreads()
      return {
        conversations: threads.map(t => ({
          id: t.thread_id,
          title: t.title || 'Untitled',
          createdAt: t.created_at ? new Date(t.created_at) : new Date(),
          updatedAt: t.updated_at ? new Date(t.updated_at) : new Date(),
        })),
        hasMore: false,
      }
    },

    // Optional: message history on conversation switch
    async listMessages({ conversationId }) {
      const { messages } = await auth.listMessages(conversationId)
      return {
        messages: messages.map(m => ({
          id: m.message_id,
          role: m.role,
          content: (m.content_blocks || [])
            .filter(b => b.type === 'text')
            .map(b => b.text)
            .join('\n'),
          createdAt: m.created_at ? new Date(m.created_at) : new Date(),
        })),
        hasMore: false,
      }
    },

    // Optional: reconnect interrupted stream
    async reconnectToStream({ messageId, signal }) {
      if (!lastSessionKey) return null
      const response = await auth.reconnectStream(lastSessionKey)
      if (!response.ok) return null
      // Reuse the same SSE → chunk transform logic as sendMessage
      // (extract to a shared helper for production use)
      return new ReadableStream<ChatMessageChunk>({
        async start(controller) {
          controller.enqueue({ type: 'start', messageId })
          let textStarted = false
          const textId = crypto.randomUUID()
          for await (const event of parseStarchildSSE(response, signal)) {
            if (event.type === 'text_delta') {
              if (!textStarted) {
                controller.enqueue({ type: 'text-start', id: textId })
                textStarted = true
              }
              controller.enqueue({ type: 'text-delta', id: textId, delta: event.text })
            } else if (event.type === 'agent_end' && textStarted) {
              controller.enqueue({ type: 'text-end', id: textId })
              textStarted = false
            }
          }
          if (textStarted) controller.enqueue({ type: 'text-end', id: textId })
          controller.enqueue({ type: 'finish', messageId })
          controller.close()
        },
      })
    },

    // Optional: server-side cancel (when abort signal is not enough)
    stop() {
      if (currentThreadId) {
        auth.cancelRun(currentThreadId)
      }
    },
  }
}

ChatBox Usage

// pages/chat.tsx
import { ChatBox } from '@mui/x-chat'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import { StarchildAuth } from 'starchild-auth-sdk'
import { createStarchildMuiAdapter } from '@/adapters/starchild-mui-adapter'

const auth = new StarchildAuth({
  clientId: process.env.NEXT_PUBLIC_STARCHILD_CLIENT_ID!,
  scope: 'profile chat',
})

const theme = createTheme() // 或你的自定义 MUI 主题
const adapter = createStarchildMuiAdapter(auth)

export default function ChatPage() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <ChatBox
        adapter={adapter}
        features={{ conversationList: true }} // 启用内置会话侧边栏
        sx={{ height: 600 }}
      />
    </ThemeProvider>
  )
}

Integration Notes

Key Point Description
Thread management SDK sendMessage When not passed, threadId automatically creates a new thread; when passed, threadId continues an existing conversation.agent_start The event returns thread_id, and the adapter should save it for subsequent requests.
Cancel / Interrupt SDK sendMessage does not accept AbortSignal parameter (SendMessageOptions has no signal field). Both adapters check in the parseStarchildSSE loop for abortSignal.aborted and then reader.cancel() stop reading the stream; MUI additionally uses stop() to call auth.cancelRun(threadId) for server-side cancellation. Note: the fetch request itself will not be aborted, it only stops consuming the response stream.
Reconnection after disconnection Save the agent_start of the session_keyevent, and after disconnection call auth.reconnectStream(sessionKey) to get a new SSE stream.
Message history auth.listMessages(threadId) returns content_blocks[], which needs to be mapped to each library's message format:text block → string / text part,tool_use block → tool-call part。
Images / Files SDK sendMessage supports the images / files / quote parameter. In the adapter, extract from messages 's attachments and pass it to SendMessageOptions。
Scope Chat integration requires scope: 'profile chat'. To display the Credits balance in the Chat interface, add credit:read。
Token refresh The SDK automatically refreshes the token every 12 minutes; the adapter does not need to worry about token expiration.onTokenRefreshFailed When triggered, guide the user to re- login()。
Tool-call chunk types For the complete tool-call chunk protocol of MUI X Chat, refer to the official documentation:https://mui.com/x/react-chat/behavior/streaming/ . The above code uses tool-call-start / tool-call-input-available / tool-result, and the latest documentation shall prevail.

Error handling

Login popup

try {
  await auth.login()
} catch (err: any) {
  if (err.message?.includes('cancelled')) {
    // 用户关闭了弹窗
  } else if (err.message?.includes('blocked')) {
    // 浏览器拦截了弹窗 — 必须在用户点击事件中调用 login()
  }
}

JSON API:StarchildAuthError

getCredits / listThreads / wallet / credit and other JSON helpers, on non-2xx, throw StarchildAuthError(exported from starchild-auth-sdk ):

import { StarchildAuth, StarchildAuthError } from 'starchild-auth-sdk'

try {
  await auth.credit.getBalance()
} catch (e) {
  if (e instanceof StarchildAuthError) {
    // e.status / e.code / e.detail / e.path / e.insufficientScope / e.response
    if (e.insufficientScope) {
      // 引导重新 login() 申请完整 scopes,或检查 OAuth App 审核 scope
      await auth.login()
    }
  }
}
Field Description
status HTTP status; when there is no response, 0
code Optional machine code
detail Raw body / detail
path Request path
insufficientScope Heuristic marker for 403 errors of the insufficient OAuth scope type
response Raw Response(if any)

SSE / Raw Response

sendMessage / reconnectStream still returns the raw Response, and you need to check ok:

const stream = await auth.sendMessage('hello')
// 或 auth.chat.send('hello')
if (!stream.ok) {
  const error = await stream.json()
  console.error('API error:', error.detail)
  if (stream.status === 401) {
    // token 过期 — SDK 会尝试自动刷新;仍失败则 onTokenRefreshFailed
  } else if (stream.status === 403) {
    // 权限不足 — scope 不满足
  }
}

Quick reference: all API endpoints

Category Endpoint Method Scope
Auth /v1/oauth/userinfo GET profile
/v1/private/oauth/authorize POST — (go-api)
/v1/oauth/refresh POST — (go-api)
/v1/oauth/logout POST — (go-api)
Bind (SDK) auth.bindAccount() → main site /?account_tab=linked-accounts — profile
auth.getBindAccountUrl() / auth.isGuest() — profile
Token (SDK) getToken() / getRefreshToken() / refreshToken() — —
Namespace auth.profile / chat / threads / messages / containers / skills / media / shares / feedback / jobs / wallet / credit — same as flat
Chat /chat/stream POST chat
/chat/stream/reconnect POST chat
/chat/runs/cancel POST chat
/chat/model GET/POST chat
Threads /api/clawd/threads GET/POST chat
/api/clawd/threads/{id} GET/DELETE chat
/api/clawd/threads/search GET chat
/api/clawd/threads/{id}/pin POST chat
/api/clawd/threads/{id}/title POST chat
Messages /api/clawd/messages GET/DELETE chat
Containers /api/cloud/containers GET chat
/api/cloud/containers/deploy POST chat
/api/cloud/containers/start POST chat
/api/cloud/containers/stop POST chat
/api/cloud/containers/restart POST chat
/api/cloud/containers/wake POST chat
/api/cloud/containers/rename PUT chat
/api/cloud/containers/status GET chat
/api/cloud/containers/metrics GET chat
/api/cloud/containers/compute-config POST chat
/api/cloud/containers/{id}/update POST chat
Skills /api/skills/catalog GET chat
/api/skills/catalog/search GET chat
/api/skills/catalog/{source}/{name} GET chat
Media /api/clawd/images/upload POST chat
/api/audio/transcribe POST chat
/v1/synthesize POST chat
Shares /api/clawd/shares GET/POST chat
/api/clawd/shares/{id} GET/DELETE chat
/api/clawd/shares/{id}/fork POST chat
Feedback /api/clawd/feedback PUT/DELETE chat
Jobs /scheduled-jobs GET/POST chat
Wallet /api/cloud/containers/portfolio/evm GET chat
/api/cloud/containers/wallets GET chat
/api/cloud/containers/wallet POST/DELETE chat
/wallet/export POST chat
Billing /coinbase/onramp-session POST chat
WebSocket /ws/sync WS chat
/ws/terminal/{id} WS chat
/ws/metrics WS chat
Notifications /v1/agentx/notifications GET/POST chat
/v1/agentx/notifications/unread-count GET chat
Models /chat/models GET chat
Free quota /v1/free-quota GET chat
Credits balance GET {creditApiBase}/api/credits GET credit:read
Credits charges GET .../api/charges GET credit:read
Credits topups GET .../api/topups GET credit:read
Credits usage GET .../api/usage/daily GET credit:read
Credits pending GET .../api/pending GET credit:read
Credits tx GET .../api/tx/{hash} GET credit:read
Stripe session POST .../api/stripe/create-session POST credit:write
Gift redeem POST .../api/gift-cards/redeem POST credit:write
Points balance GET .../api/points/balance GET credit:read
Points exchange POST .../api/points/exchange POST credit:write
KYC GET/POST .../api/kyc/* * read/write
Referral GET/POST .../api/referral/* * read/write
Migration reward GET/POST .../api/migration/reward/* * read/write
WOO bonus GET .../api/public/users/{id}/woo-bonus GET public

Local testing guide (required reading for Agents)

Execution principles for Agents:OAuth login() must bereal browser page(requires popup + user gesture + page Origin). After obtaining the token, the API can be tested with curl/Node scripts. It is forbidden to assume that "pure Node without Origin can run popup login successfully".

1. Two sets of Origins, do not mix them

Role Origin example Configured by
Main site web (authorization popup) http://localhost:6066 and https://localhost:6066 serverstatic CORS has been uniformly allowed (go-api CORS_ORIGINS / ai-agent CORS_ALLOWED_ORIGINS / clawd CORS_ORIGINS; the production env should also include these two items)
Third-party app page (site integrating the SDK) e.g., http://localhost:3333、http://localhost:5173 must be entered into that OAuth App's allowed_origins(filled in during registration/review; must match the page address bar's exactly, including scheme and port)
  • The popup opens the main site (local web or https://iamstarchild.com), and the browser Origin is the main site.
  • The SDK runs on the third-party page; the API request's Origin is the third-party origin → it is dynamically merged into CORS via the OAuth client.
  • authorize when: Header Origin = main site (trusted web), body origin = third-party origin (validated against the client whitelist).

2. Production default Base URL (SDK 0.4.x)

Aligned with starchild-web,defaults to production; local demos generally don't need changes:

Config item Default
origin https://iamstarchild.com(popup main site)
apiBase https://go-api.iamstarchild.com/v1
chatApiBase https://ai-api.iamstarchild.com
clawdApiBase https://preview.iamstarchild.com(HTTP chat/stream、jobs)
clawdWsBase wss://preview.iamstarchild.com(WS)
creditApiBase https://credit.iamstarchild.com

Local integrationfull backendwhen, then explicitly change to http://127.0.0.1:8000/v1、http://127.0.0.1:8008、http://127.0.0.1:8009 , etc. (see the repo clinerules port table).

3. Scenario A — Third-party local page + production API (most common)

Goal: run the integrator page on http://localhost:<port> , with login and API hitting production.

  1. OAuth App allowed_origins includes the page Origin (e.g., http://localhost:3333), status=approved, scopes sufficient.
  2. The page uses the SDK:clientId + the needed scope;do notconfigure the third-party origin as 6066.
  3. Open the third-party page in a browser → user clicks → auth.login() → popup goes to production https://iamstarchild.com(default origin)。
  4. After successful login, call on the same page auth.chat / auth.credit / SSE / WS。
  5. If CORS fails: check whether the third-party origin is in the client whitelist; whether the static CORS of production go-api/ai-agent/clawd includes the main site-related domains (the 6066 is only needed when testing the popup with local web).

In-repo reference:

  • SDK demo:starchild-auth-sdk/example/index.html、test-sdk-full.html(default production URL)
  • Orderly example:orderly-test-dex + starchild-orderly-plugin

4. Scenario B — Local main site web (6066) + local or production API

Goal: modify starchild-web / test the authorize intermediary, Guest binding, etc.

  1. Start starchild-web: default http://localhost:6066(you can also use a local https certificate → https://localhost:6066)。
  2. Confirm the API-side static CORS includes:
    • http://localhost:6066
    • https://localhost:6066
    • (optional)https://starchild.dev:6066
  3. Env name:
    • go-api:CORS_ORIGINS(after setting,the entire table replacesthe code defaults, and must retain both the production domain + the above 6066)
    • ai-agent:CORS_ALLOWED_ORIGINS(same as above)
    • clawd:CORS_ORIGINS(the entrypoint already includes 6066 http/https by default)
  4. Third parties still register with their own origin; the local web is only responsible for the popup/intermediary.

5. Scenario C — Browser vs Node/script

Step Browser curl / Node
login() popup Required Cannot(no window, no real page Origin)
Call userinfo / threads / credits with a token Yes Yes (Authorization: Bearer)
automatically includes CORS Browser execution Script has no CORS; direct connection is fine
Test whether CORS is allowed DevTools / page request OPTIONS + Origin header to simulate preflight

Agent recommended workflow:

  1. Complete login in the browser, get the access token from onLogin / DevTools / auth.getToken() (refresh is for debugging only getRefreshToken(), do not share externally).
  2. Then use curl to run the matrix (scope gates, 403, credits, etc.).
  3. When regression of popup/CORS is needed, then use Playwright/real page,the origin of the page URL must already be in the client whitelist。

6. CORS preflight (Agent can run directly)

# 期望:ACAO 回显同一 Origin(静态主站 web)
curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: http://localhost:6066' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type' \
  | tr -d '\r' | grep -i access-control-allow-origin

curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: https://localhost:6066' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type' \
  | tr -d '\r' | grep -i access-control-allow-origin

# 第三方本地 origin(须已在该 OAuth client allowed_origins,且 client approved)
curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: http://localhost:3333' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type' \
  | tr -d '\r' | grep -i access-control-allow-origin

# 期望:无 ACAO(拒绝)
curl -s -D - -o /dev/null -X OPTIONS 'https://go-api.iamstarchild.com/v1/oauth/refresh' \
  -H 'Origin: https://evil.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  | tr -d '\r' | grep -i access-control-allow-origin || echo 'DENY_OK'

For the local backend, just change host to http://127.0.0.1:8000 / :8008 / :8009 .

7. API smoke test after login (once token is available)

TOKEN='<access_token from browser login>'
# profile
curl -s -H "Authorization: Bearer $TOKEN" https://ai-api.iamstarchild.com/v1/oauth/userinfo
# chat(无 chat scope 应 403)
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOKEN" \
  https://ai-api.iamstarchild.com/api/clawd/threads
# credits(需 credit:read)
curl -s -H "Authorization: Bearer $TOKEN" https://credit.iamstarchild.com/api/credits

8. Local full-stack ports (repository development)

Service Port
starchild-web http://localhost:6066
go-api http://127.0.0.1:8000
ai-agent http://127.0.0.1:8008
clawd http://127.0.0.1:8009
credit-api Deploy per transparent-proxy (production credit.iamstarchild.com)

Dynamic CORS: go-api / ai-agent / clawd periodically pull approved + active OAuth client's allowed_origins and merge into the whitelist (default about 5 minutes; if it does not take effect after changing origin, restart the service or wait for refresh).

9. Security notes (during testing)

  • Static allowlist is limited to exact http://localhost:6066 and https://localhost:6066, not any arbitrary localhost port.
  • For third parties, each port/scheme must be added separately to allowed_origins。
  • Do not write refresh token into logs, issues, or send to user chat.
  • When production env overrides CORS, it mustfull tableinclude the online domain + the required local web origin, to avoid configuring only localhost and causing all cross-origin on the main site to fail.

10. Agent checklist (check before testing SDK / OAuth)

  • OAuth client allowed_origins exactly equalthird-party test page Origin
  • client status=approved and is_active, scopes include the capability to be tested
  • login() called in the browser click path
  • For the main site local popup, API CORS includes http://localhost:6066 and https://localhost:6066
  • SDK base URL: use default for production; change apiBase/chatApiBase/clawd* for local backend
  • For CORS failure, first distinguish whether it is "static main site origin" or "OAuth third-party origin"
  • scope matrix: profile-only cannot threads; credit:read cannot write
  • Script testing only afteralready has token then proceed

Troubleshooting

Issue Cause Solution
Popup blocked Browser requires user gesture trigger Ensure login() is called in the click handler
Origin mismatch Allowed Origin configuration does not match Check the origin of OAuth Apps andthird-party pageare exactly the same (including http/https and port)
Local web popup CORS 6066 not in static CORS Confirm go-api/ai-agent/clawd (and production env) include http://localhost:6066 and https://localhost:6066
Third-party local CORS origin not in client or not in dynamic list Write into allowed_origins and approved; wait for CORS refresh or restart API
Node script cannot login popup depends on browser After obtaining token via browser login, use script to call API
onLogin not triggered User did not confirm authorization or popup was closed Check onAuthCancelled and onAuthError
SSE stream interrupted Network fluctuation or container restart Use session_key to call reconnectStream()
WS disconnected Connection timeout or server restart Implement exponential backoff for automatic reconnection
403 on container delete OAuth token does not allow deleting container This is expected behavior and cannot be bypassed
401 after token refresh refresh token expired Guide user to login() again
Guest needs to link to a full account userInfo.isGuest === true Call auth.bindAccount() to redirect to the main site Linked accounts, do not self-build /guest/bind; Guest login itself goes through login() main site popup, no loginAsGuest
need refresh token Self-built refresh or debugging Use getRefreshToken(); prefer SDK refreshToken(); do not log/leak
JSON API throw non-2xx catch StarchildAuthError, see insufficientScope
403 Insufficient scope credit:* token does not contain credit scope login again and request credit:read/credit:write; check whether the OAuth App has been approved
Credits call fails / CORS origin not in client allowed origins Same as chat, origin must be in the OAuth client allowlist
Stripe succeeded but balance unchanged Still pending or tx not credited Poll getCreditTxStatus / getPendingCredit / getCredits