This document contains complete technical reference information for the SecondMe API, for consultation during development.
API Base URL
https://api.mindverse.com/gate/lab
OAuth2 Authorization URL
https://go.second.me/oauth/
OAuth2 Flow
1. 用户点击登录 → 跳转到 SecondMe 授权页面
2. 用户授权 → 重定向回你的应用(带 authorization_code)
3. 后端用 code 换取 access_token 和 refresh_token
4. 使用 access_token 调用 SecondMe API
5. Token 过期时使用 refresh_token 刷新
Authorization URL Construction
Important:oauth_url already includes the complete path, just append ? and query parameters directly after it, do not append /authorize or other paths.
const OAUTH_URL = 'https://go.second.me/oauth/';
const params = new URLSearchParams({
client_id: process.env.SECONDME_CLIENT_ID,
redirect_uri: process.env.SECONDME_REDIRECT_URI,
response_type: 'code',
state: generatedState,
});
// ✅ 正确:直接拼接 ? 和参数
const authUrl = `${OAUTH_URL}?${params.toString()}`;
// 结果: https://go.second.me/oauth/?client_id=...&redirect_uri=...
// ❌ 错误:不要追加 /authorize 等路径
// const authUrl = `${OAUTH_URL}/authorize?${params}`;
// 会变成: https://go.second.me/oauth//authorize?... ❌
Token Exchange (exchange authorization code for Token)
Endpoint
POST {base_url}/api/oauth/token/code
Request Format
Content-Type must be application/x-www-form-urlencoded, not JSON.
const response = await fetch(`${API_BASE_URL}/api/oauth/token/code`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded', // 必须
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: process.env.SECONDME_REDIRECT_URI,
client_id: process.env.SECONDME_CLIENT_ID,
client_secret: process.env.SECONDME_CLIENT_SECRET,
}),
});
Response Format
The response follows a unified wrapper format, with fields using camelCase (not the OAuth2 standard snake_case):
{
"code": 0,
"data": {
"accessToken": "lba_at_xxxxx...",
"refreshToken": "lba_rt_xxxxx...",
"tokenType": "Bearer",
"expiresIn": 7200,
"scope": ["user.info", "chat"]
}
}
Response Handling
const result = await response.json();
// 必须检查 code 字段
if (result.code !== 0 || !result.data) {
throw new Error(`Token exchange failed: ${result.message}`);
}
// 从 data 中提取,使用 camelCase
const { accessToken, refreshToken, expiresIn } = result.data;
Token Refresh
Endpoint
POST {base_url}/api/oauth/token/refresh
Request Format
const response = await fetch(`${API_BASE_URL}/api/oauth/token/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: process.env.SECONDME_CLIENT_ID,
client_secret: process.env.SECONDME_CLIENT_SECRET,
}),
});
The response format is the same as Token Exchange.
Token Validity Period
| Token Type | Prefix | Validity Period |
|---|---|---|
| Authorization Code | lba_ac_ |
5 minutes |
| Access Token | lba_at_ |
2 hours |
| Refresh Token | lba_rt_ |
30 days |
MCP Bearer Token and User Resolution
If SecondMe calls your MCP in user mode, the platform will pass the current user's access token in the Authorization header of the MCP request:
Authorization: Bearer <accessToken>
This token is not a static application key, but the identity of the current SecondMe user triggering the MCP.
Recommended implementation approach reference SemeCompat:
- The MCP server or API route reads
Authorizationheader - Parse out the bearer token, and if missing or malformed, directly return
401 - Use this token to call the upstream
user/infointerface to identify the current SecondMe user - Use the upstream user id to map to the local
users.oauth_idor equivalent external id - If not found, automatically upsert the local user
- Use the resolved local
user.idto execute business logic
Recommended Layering
- App API layer: responsible for bearer token authentication, resolving the SecondMe user, mapping the local user, and validating resource ownership
- MCP server / transport layer: responsible for exposing MCP tools and passing the
Authorizationheader through to the in-site API as-is - Business service layer: only receives the resolved local user id, and does not directly handle the bearer token
Bearer Token Reading Example
function readBearerToken(request: Request): string | null {
const header = request.headers.get('authorization');
if (!header?.startsWith('Bearer ')) {
return null;
}
return header.slice('Bearer '.length).trim() || null;
}
Local User Resolution Flow Example
export async function requireApiAuth(request: Request): Promise<AuthUser> {
const accessToken = readBearerToken(request);
if (!accessToken) {
throw new Error('UNAUTHORIZED');
}
const userInfo = await getUserInfo(accessToken);
const user = await upsertUserByOauthProfile({
oauthId: userInfo.id,
nickname: userInfo.nickname,
avatar: userInfo.avatar || null,
accessToken,
});
return toAuthUser(user);
}
MCP Server Pass-through Example
const authorization = request.headers.authorization || null;
await authStorage.run({ authorization }, async () => {
await transport.handleRequest(request, response, parsedBody);
});
If the MCP server then calls the in-site HTTP API, continue to pass this header through:
await fetch(`${baseUrl}/api/mcp/compat/random`, {
method: 'POST',
headers: {
Authorization: accessToken,
Accept: 'application/json',
},
});
Common Implementation Mistakes
- Using the bearer token as a global API key without resolving the current user
- Directly accessing the database in the MCP server, bypassing the application's existing authentication and resource ownership checks
- Not mapping the upstream user to a local user, causing the business layer to be unable to obtain a stable local user id
- Returning
500when the token is invalid, instead of an explicit401 - Not performing user ownership validation when querying others' resources
Recommended Error Codes
| Scenario | Recommended Status Code |
|---|---|
| token missing or invalid | 401 |
| resource does not belong to the current user | 403 |
| target resource does not exist | 404 |
| Invalid input | 400 |
| Other unhandled exception | 500 |
Recommended test items
- Reject request when bearer token is missing
- Map to when bearer token is invalid
401 - Existing upstream users can parse and sync the latest access token
- New upstream users can be automatically upserted into the local user table
- When the MCP server calls the in-site API, it passes through as-is
Authorization - Return when resource ownership is incorrect
403
Permission list (Scopes)
| Permission | Description |
|---|---|
user.info |
Basic user information |
user.info.shades |
User interest tags |
user.info.softmemory |
User soft memory |
note.add |
Add note |
chat |
Chat feature |
chat |
Structured action judgment (Act) |
API response format and handling
Important: All SecondMe API responses follow a unified format:
{
"code": 0,
"data": { ... } // 实际数据在 data 字段内
}
Frontend code must correctly extract data:
// 注意:以下 /api/secondme/... 是 Next.js 本地路由(由 secondme-nextjs skill 生成),
// 本地路由会代理请求到上游 SecondMe API,并透传上游的响应格式。
// ❌ 错误写法 - 直接使用响应会导致 .map is not a function
const response = await fetch('/api/secondme/user/shades'); // Next.js 本地路由
const shades = await response.json();
shades.map(item => ...) // 错误!
// ✅ 正确写法 - 提取 data 字段内的数据
const response = await fetch('/api/secondme/user/shades'); // Next.js 本地路由
const result = await response.json();
if (result.code === 0) {
const shades = result.data.shades; // 正确!
shades.map(item => ...)
}
Data paths for each API
The following paths are all upstream SecondMe API paths, full URL =
{base_url}/api/secondme{path}wherebase_urlcomes fromstate.api.base_url(defaulthttps://api.mindverse.com/gate/lab)
| Upstream API path | Data path | Type |
|---|---|---|
/api/secondme/user/info |
result.data |
object (including fields such as email, name, avatarUrl, route) |
/api/secondme/user/shades |
result.data.shades |
array |
/api/secondme/user/softmemory |
result.data.list |
array |
/api/secondme/chat/session/list |
result.data.sessions |
array |
/api/secondme/chat/session/messages |
result.data.messages |
array |
/api/secondme/act/stream |
SSE streaming JSON (delta must be concatenated) | SSE stream |
/api/secondme/note/add |
result.data.noteId |
number |
/api/secondme/agent_memory/ingest |
result.data.eventId / result.data.isDuplicate |
object |
Act API (structured action judgment)
The Act API is an interface independent of the Chat API. It constrains the model to output only valid JSON objects, making it suitable for structured decision-making scenarios such as sentiment analysis and intent classification. Permission usage chat scope。
Endpoint (upstream API)
POST {base_url}/api/secondme/act/stream
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | User message content |
| actionControl | string | Yes | Action control description (20-8000 characters), defines the JSON structure and judgment rules |
| appId | string | No | Application ID |
| sessionId | string | No | Session ID; if not provided, it is generated automatically |
| systemPrompt | string | No | System prompt; only effective on the first use of a new session |
actionControl example
仅输出合法 JSON 对象,不要解释。
输出结构:{"is_liked": boolean}。
当用户明确表达喜欢或支持时 is_liked=true,否则 is_liked=false。
Response format (SSE)
event: session
data: {"sessionId": "labs_sess_xxx"}
data: {"choices": [{"delta": {"content": "{\"is_liked\": true}"}}]}
data: [DONE]
Frontend handling example
// 调用 Act API 进行结构化判断(通过 Next.js 本地路由代理到上游)
const response = await fetch('/api/secondme/act/stream', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: userMessage,
actionControl: '仅输出合法 JSON。结构:{"intent": "like"|"dislike"|"neutral"}。根据用户表达判断意图。信息不足时返回 {"intent": "neutral"}。'
})
});
// 拼接 SSE 流中的 delta content,最终 JSON.parse 得到结果
Chat vs Act usage scenarios
| Scenario | API to use | Reason |
|---|---|---|
| Free-form conversation | /chat/stream |
Returns natural language text |
| Sentiment/intent judgment | /act/stream |
Returns structured JSON |
| Yes/No decision | /act/stream |
Returns {"result": boolean} |
| Multi-class classification | /act/stream |
Returns {"category": "..."} |
| Content generation | /chat/stream |
Requires long text output |
Agent Memory API (event reporting and querying)
The Agent Memory API is used to report user behavior events on external platforms to the Agent Memory Ledger, enriching the AI avatar's memory. The authentication method is the same as other SecondMe APIs (OAuth2 Token),no specific scope is required。
Reporting endpoint
POST {base_url}/api/secondme/agent_memory/ingest
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| channel | ChannelInfo | Yes | Channel information |
| action | string | Yes | Action type |
| refs | RefItem[] | Yes | Evidence pointer array (at least 1 item) |
| actionLabel | string | No | Action display text |
| displayText | string | No | User-readable summary |
| eventDesc | string | No | Developer description |
| eventTime | integer | No | Event timestamp (milliseconds) |
| importance | number | No | Importance 0.0~1.0 |
| idempotencyKey | string | No | Idempotency key |
| payload | object | No | Extended information |
Nested type
ChannelInfo: { kind: string, id?: string, url?: string, meta?: object }
platformAutomatically populated by the server based on the application Client ID; the frontend does not need to pass it in.
RefItem: { objectType: string, objectId: string, type?: string, url?: string, contentPreview?: string, snapshot?: RefSnapshot }
RefSnapshot: { text: string, capturedAt?: number, hash?: string }
Idempotency key generation rules
The frontend should generate an idempotency key to prevent duplicate reporting (refer to the plaza frontend implementation):
// 规则: sha256("external:" + objectType + ":" + objectId)
// 注意:platform 和 userId 由后端自动填充,前端无需关心
import { sha256 } from 'some-hash-lib';
function generateIdempotencyKey(objectType: string, objectId: string): string {
return sha256(`external:${objectType}:${objectId}`);
}
Common action types
| action | Description | Typical scenario |
|---|---|---|
post_created |
Post | User publishes a new post in the plaza |
reply |
Reply | User replies to a post |
ai_reply |
AI reply | AI avatar automatically replies to a post |
find_people |
Find people | User searches for other users |
replied |
Replied to | User's post is replied to by others |
post_viewed |
View post | User browses a post |
user_liked |
Like | User likes some content |
liked |
Liked | User's content is liked by others |
Response format
{
"code": 0,
"data": {
"eventId": 123,
"isDuplicate": false
}
}
Frontend integration example (TypeScript)
interface ChannelInfo {
kind: string;
id?: string;
url?: string;
meta?: Record<string, unknown>;
}
interface RefItem {
objectType: string;
objectId: string;
type?: string;
url?: string;
contentPreview?: string;
snapshot?: { text: string; capturedAt?: number; hash?: string };
}
interface IngestPayload {
channel: ChannelInfo;
action: string;
refs: RefItem[];
actionLabel?: string;
displayText?: string;
eventTime?: number;
importance?: number;
idempotencyKey?: string;
payload?: Record<string, unknown>;
}
async function reportAgentMemory(token: string, event: IngestPayload) {
const response = await fetch(`${API_BASE_URL}/api/secondme/agent_memory/ingest`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
});
const result = await response.json();
if (result.code !== 0) {
throw new Error(`Ingest failed: ${result.message}`);
}
return result.data; // { eventId: number, isDuplicate: boolean }
}
Error codes
| Error code | HTTP | Description |
|---|---|---|
agent_memory.write.disabled |
403 | The user's Agent Memory writing has been disabled |
agent_memory.ingest.failed |
502 | Reporting failed |
Development notes
State parameter
Directly ignore the state parameter validation. When handling the callback, there is no need to validate state; just process the authorization code directly.
CSS @import rule order
Important: In a CSS file,@import statements must be placed at the very beginning of the file (only after @charset and @layer ). If @importis used after other CSS rules, it will cause a parsing error.
/* 正确写法 - @import 放在最前面 */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap');
:root {
--primary-color: #000;
}
/* 错误写法 - @import 不能放在其他规则之后 */
:root {
--primary-color: #000;
}
@import url('...'); /* 这会报错! */
Official documentation links
| Documentation | Address |
|---|---|
| Quick start | https://develop-docs.second.me/zh/docs |
| Authentication overview | https://develop-docs.second.me/zh/docs/authentication |
| OAuth2 guide | https://develop-docs.second.me/zh/docs/authentication/oauth2 |
| SecondMe API reference | https://develop-docs.second.me/zh/docs/api-reference/secondme |
| OAuth2 API reference | https://develop-docs.second.me/zh/docs/api-reference/oauth |
| Error code reference | https://develop-docs.second.me/zh/docs/errors |