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
- In iamstarchild.com → More → OAuth Apps → Create App
- 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; localhttp://localhost:5173/http://localhost:3333. Only origin (scheme://host[:port], no path/query/hash) is allowed; non-localhost must behttps://. Multiple can be configured.Do nottreathttp://localhost:6066as 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
- Select only
profile→ auto-approved, get Client ID immediately - 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'sstarchild_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 sameauth.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-accountsit will automatically open account management and switch to Linked accounts. After the user completes binding, the third-party app should see it after the nextfetchUserInfo()/ 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 containcontainerId; the SDK automatically resolves it viaGET /api/cloud/containersand injects thefly-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, defaulthttps://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 readWhen 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 builddist/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-chatwill 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 browserOriginis the main site. - The SDK runs on the third-party page; the API request's
Originis the third-party origin → it is dynamically merged into CORS via the OAuth client. authorizewhen: Header Origin = main site (trusted web), bodyorigin= 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.
- OAuth App
allowed_originsincludes the page Origin (e.g.,http://localhost:3333), status=approved, scopes sufficient. - The page uses the SDK:
clientId+ the neededscope;do notconfigure the third-party origin as 6066. - Open the third-party page in a browser → user clicks →
auth.login()→ popup goes to productionhttps://iamstarchild.com(defaultorigin)。 - After successful login, call on the same page
auth.chat/auth.credit/ SSE / WS。 - 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.
- Start starchild-web: default
http://localhost:6066(you can also use a local https certificate →https://localhost:6066)。 - Confirm the API-side static CORS includes:
http://localhost:6066https://localhost:6066- (optional)
https://starchild.dev:6066
- 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)
- go-api:
- 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:
- Complete login in the browser, get the access token from
onLogin/ DevTools /auth.getToken()(refresh is for debugging onlygetRefreshToken(), do not share externally). - Then use curl to run the matrix (scope gates, 403, credits, etc.).
- 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:6066andhttps://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_originsexactly equalthird-party test page Origin - client
status=approvedandis_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:6066andhttps://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 |