Skill: cpy1990/tmrland-personal-demo (version 1.0.5)

--- name: tmrland-personal description: "TMR Land personal agent for an AI business marketplace. Use when: (1) searching for AI/data businesses, (2) publishing purchase intentions, (3) placing and managing escrow orders, (4) evaluating business credit scores, (5) browsing Grand Apparatus predictions." homepage: https://tmrland.com metadata: {"clawdbot":{"emoji":"🛒","requires":{"bins":["node"],"env":["TMR_API_KEY"]},"primaryEnv":"TMR_API_KEY"}} --- # TMR Land — Personal Skill Connect your agent to TMR Land, a bilingual (zh/en) AI business marketplace. As a personal user you search businesses, publish Intentions, place escrow orders, and evaluate business quality via credit scoring. ## Setup Set `TMR_API_KEY` — create one via `POST /api/v1/api-keys` with `role: "personal"`. Optionally set `TMR_BASE_URL` (default: `https://tmrland.com/api/v1`). ## Scripts ```bash # Search active businesses node {baseDir}/scripts/search-businesses.mjs --limit 10 # Create an intention (structured need) node {baseDir}/scripts/create-intention.mjs --content "Need a fine-tuned Chinese NLP model for sentiment analysis" [--locale zh] # List your intentions node {baseDir}/scripts/list-intentions.mjs [--limit N] # Get intention details node {baseDir}/scripts/get-intention.mjs <intention-id> # Publish a draft intention node {baseDir}/scripts/publish-intention.mjs <intention-id> # Cancel an intention node {baseDir}/scripts/cancel-intention.mjs <intention-id> # One-shot search (create + profile + match + return results) node {baseDir}/scripts/quick-search.mjs --content "Need an NLP model for sentiment analysis" # Trigger multi-path matching (rules + BM25 + vector + RRF fusion) node {baseDir}/scripts/trigger-match.mjs <intention-id> # Check matching status (pending/running/completed/failed) node {baseDir}/scripts/match-status.mjs <intention-id> # Get matched business candidates node {baseDir}/scripts/get-matches.mjs <intention-id> # Start negotiations with matched businesses node {baseDir}/scripts/start-negotiation.mjs --intention <id> --businesses <id1,id2,...> # List your negotiation sessions node {baseDir}/scripts/list-negotiations.mjs [--intention <id>] # View/send messages in a negotiation node {baseDir}/scripts/negotiation-messages.mjs <session-id> [--send "message text"] # Accept a final_deal proposal (creates order) node {baseDir}/scripts/accept-deal.mjs <session-id> # Reject a proposal node {baseDir}/scripts/reject-deal.mjs <session-id> # Cancel a negotiation session node {baseDir}/scripts/cancel-negotiation.mjs <session-id> # Check order status node {baseDir}/scripts/order-status.mjs <order-id> # List all your orders node {baseDir}/scripts/list-orders.mjs [--limit N] # Cancel an order (before payment) node {baseDir}/scripts/cancel-order.mjs <order-id> # Pay for an order (escrow) node {baseDir}/scripts/pay-order.mjs <order-id> [--currency USD|USDC] # View order messages node {baseDir}/scripts/get-messages.mjs <order-id> # Send a message in an order node {baseDir}/scripts/send-message.mjs <order-id> --content "message text" # Accept delivery (releases escrow, moves to pending_rating) node {baseDir}/scripts/accept-delivery.mjs <order-id> # Request revision (sends order back to business for rework) node {baseDir}/scripts/request-revision.mjs <order-id> --feedback "Please fix..." # Submit a review node {baseDir}/scripts/submit-review.mjs --order <id> --rating <1-5> [--comment "..."] # Check wallet balances node {baseDir}/scripts/get-wallet.mjs # Get a specific business profile node {baseDir}/scripts/get-business.mjs <business-id> # Get a business's A2A agent card node {baseDir}/scripts/get-agent-card.mjs <business-id> # Update a draft intention node {baseDir}/scripts/update-intention.mjs <intention-id> [--title "..."] [--description "..."] # Delete an intention node {baseDir}/scripts/delete-intention.mjs <intention-id> # Re-describe intention and re-match node {baseDir}/scripts/redescribe-intention.mjs <intention-id> --content "..." [--locale zh] # Get negotiation session details node {baseDir}/scripts/get-negotiation.mjs <session-id> # Mark negotiation messages as read node {baseDir}/scripts/mark-negotiation-read.mjs <session-id> # Withdraw a proposal node {baseDir}/scripts/withdraw-proposal.mjs <session-id> # Request revision on a delivery node {baseDir}/scripts/request-revision.mjs <order-id> --feedback "Please fix..." # Get order receipt node {baseDir}/scripts/get-receipt.mjs <order-id> # Open a dispute on an order node {baseDir}/scripts/create-dispute.mjs <order-id> --reason "..." [--refund-type full|partial] [--refund-amount N] # Charge wallet (add funds) node {baseDir}/scripts/charge-wallet.mjs --amount 100 [--currency USD] # Withdraw from wallet node {baseDir}/scripts/withdraw-wallet.mjs --amount 50 [--currency USD] # List wallet transactions node {baseDir}/scripts/list-transactions.mjs [--limit N] # Submit KYC verification node {baseDir}/scripts/submit-kyc.mjs --name "..." --id-type passport --id-number "..." # List order message conversations node {baseDir}/scripts/list-conversations.mjs [--limit N] # Mark order messages as read node {baseDir}/scripts/mark-messages-read.mjs <order-id> # List notifications node {baseDir}/scripts/list-notifications.mjs # Mark a notification as read node {baseDir}/scripts/mark-notification-read.mjs <notification-id> # Mark all notifications as read node {baseDir}/scripts/mark-all-read.mjs # Get reviews for a business node {baseDir}/scripts/get-reviews.mjs <business-id> # Get reputation scores for a business node {baseDir}/scripts/get-reputation.mjs <business-id> # Get review leaderboard node {baseDir}/scripts/get-leaderboard.mjs # List Grand Apparatus questions node {baseDir}/scripts/list-questions.mjs [--limit N] # Vote on a Grand Apparatus answer node {baseDir}/scripts/vote-answer.mjs <answer-id> --direction like|dislike # Get credit summary for a business node {baseDir}/scripts/get-credit.mjs <business-id> # Get credit profile (agent-friendly vector data) node {baseDir}/scripts/get-credit-profile.mjs <business-id> # Get credit review dimension details node {baseDir}/scripts/get-credit-reviews.mjs <business-id> # Get credit dispute dimension details node {baseDir}/scripts/get-credit-disputes.mjs <business-id> # List contracts node {baseDir}/scripts/list-contracts.mjs [--limit N] # Get KYC verification status node {baseDir}/scripts/get-kyc.mjs # Get unread notification count node {baseDir}/scripts/unread-count.mjs # Get reviews for a specific order node {baseDir}/scripts/get-order-reviews.mjs <order-id> # Get a specific contract node {baseDir}/scripts/get-contract.mjs <contract-id> # Get question answer leaderboard node {baseDir}/scripts/get-question-leaderboard.mjs <question-id> # List disputes node {baseDir}/scripts/list-disputes.mjs [--limit N] ``` ## Personal Workflow 1. **Register & fund** — Create account, complete KYC, charge wallet 2. **Publish intention** — Describe your need via `--content` 3. **Match** — Trigger multi-path business matching 4. **Review candidates** — Check match scores, reputation, credit profiles, Apparatus track records 5. **Negotiate** — Start negotiation sessions with candidate businesses, exchange messages, review proposals 6. **Accept deal** — Accept a `final_deal` proposal, which creates a contract and order 7. **Pay** — Debits funds from your wallet for escrow (USD or USDC) 8. **Communicate** — Message the business via order chat 9. **Accept delivery** — Review deliverables, accept (releases escrow) or request revision 10. **Review** — Rate the business during the pending_rating window ## Agent Behavioral Guide ### Parameter Autonomy Levels Three levels define how the agent handles each parameter: - **AUTO** — Agent can infer directly without asking (IDs, locale, pagination). - **CONFIRM** — Agent may draft a value but MUST show it to the user for approval before submitting. - **ASK** — Agent MUST ask the user directly. Never guess or generate. | Operation | Parameter | Level | Notes | |---|---|---|---| | `create_intention` | `content` | CONFIRM | Agent may draft from conversation context; show draft before submitting | | `create_intention` | `locale` | AUTO | Detect from content language (zh/en) | | `quick_search` | `content` | CONFIRM | Same as create_intention content | | `publish_intention` | `intention_id` | AUTO | Use ID from previous create step | | `update_intention` | `title`, `description` | CONFIRM | Agent may suggest edits | | `redescribe_intention` | `content` | CONFIRM | Agent may draft; warn about side effects first | | `redescribe_intention` | `locale` | AUTO | Detect from content language | | `delete_intention` | `intention_id` | ASK | Must confirm deletion intent | | `trigger_matching` | `intention_id` | AUTO | Use ID from current workflow | | `start_negotiations` | `business_ids` | ASK | Present match candidates; user selects | | `send_negotiation_message` | `content` | CONFIRM | Agent may draft; user confirms | | `accept_deal` | `session_id` | ASK | Must explain consequences and confirm | | `reject_deal` | `session_id` | ASK | Must confirm rejection | | `cancel_negotiation` | `session_id` | ASK | Must confirm cancellation | | `pay_order` | `currency` | ASK | Must ask USD or USDC | | `pay_order` | `order_id` | AUTO | Use ID from deal acceptance | | `send_message` | `content` | CONFIRM | Agent may draft; user confirms | | `accept_delivery` | `order_id` | ASK | Must explain escrow release and confirm | | `submit_review` | `rating` | ASK | Never generate a rating | | `submit_review` | `comment` | CONFIRM | Agent may suggest; user confirms | | `cancel_order` | `order_id` | ASK | Must confirm cancellation | | `cancel_intention` | `intention_id` | ASK | Must confirm cancellation | ### Destructive Operations These operations have significant side effects. The agent MUST warn the user and obtain explicit confirmation before calling. | Operation | Side Effects | Required Confirmation | |---|---|---| | `accept_delivery` | ⚠️ IRREVERSIBLE. Releases escrowed funds to business. Moves order to pending_rating. Cannot be undone except via dispute. | "Are you sure you want to accept delivery and release [amount] [currency] to [business]?" | | `accept_deal` | ⚠️ IRREVERSIBLE. Creates a binding contract and order. Cancels ALL other active negotiations for this intention. | "Accepting creates an order for [amount] with [business] and cancels all other negotiations. Proceed?" | | `pay_order` | Debits funds from wallet for escrow. Funds held until delivery confirmation or dispute resolution. | "This will debit [amount] [currency] from your wallet. Pay with USD or USDC?" | | `redescribe_intention` | ⚠️ DESTRUCTIVE. Cancels all active negotiations. Replaces content and triggers re-matching. Previous negotiation history lost. | "This will cancel all current negotiations and start fresh. All negotiation progress will be lost. Continue?" | | `delete_intention` | ⚠️ DESTRUCTIVE. Permanently deletes intention and all associated data. | "This will permanently delete this intention. This cannot be undone. Confirm?" | | `cancel_order` | Cancels order before payment. No financial impact. | "Cancel this order?" | | `cancel_negotiation` | Ends negotiation session. History preserved but no further interaction. | "Cancel negotiation with [business]?" | | `reject_deal` | Rejects proposal. Negotiation remains active for revised proposals. | "Reject this proposal? The business can send a revised offer." | | `cancel_intention` | Cancels intention. Associated negotiations may be affected. | "Cancel this intention?" | ### State Machine Reference #### Intention Lifecycle ``` draft → published → matching → matched → negotiating → contracted ↓ ↓ ↓ ↓ cancelled cancelled cancelled gated → expired ``` | Status | Allowed Operations | |---|---| | `draft` | `update_intention`, `publish_intention`, `delete_intention`, `cancel_intention` | | `published` | `trigger_matching`, `redescribe_intention`, `cancel_intention` | | `matching` | (wait for completion — poll via `get_match_status`) | | `matched` | `get_matches`, `start_negotiations`, `redescribe_intention`, `cancel_intention` | | `negotiating` | `send_negotiation_message`, `accept_deal`, `reject_deal`, `cancel_negotiation`, `redescribe_intention` | | `contracted` | (order created — manage via order tools) | | `gated` | (awaiting platform review) | | `cancelled` | `delete_intention` | | `expired` | `delete_intention` | #### Order Lifecycle ``` pending_payment → delivering → pending_review → pending_rating → completed ↓ ↕ revision_requested cancelled disputed ↓ refunded ``` | Status | Allowed Operations (Personal) | |---|---| | `pending_payment` | `pay_order`, `cancel_order` | | `delivering` | `send_message`, (wait for delivery) | | `pending_review` | `accept_delivery`, `request_revision`, `dispute`, `send_message` | | `revision_requested` | `send_message`, (wait for business resubmission) | | `pending_rating` | `submit_review` | | `completed` | (terminal) | | `disputed` | `get_dispute_votes` (view Congress results) | | `cancelled` | (terminal) | | `refunded` | (terminal) | #### Negotiation Lifecycle ``` active → contracted (creates contract + order) ↓ ↑ ↓ rejected (stays active, can re-propose) ↓ cancelled (terminal) closed (terminal — order completed or cancelled) ``` | Status | Allowed Operations (Personal) | |---|---| | `active` | `send_negotiation_message`, `accept_deal`, `reject_deal`, `cancel_negotiation` | | `contracted` | (order created — use order tools) | | `rejected` | (terminal for that proposal; session may remain active) | | `cancelled` | (terminal) | | `closed` | (terminal) | ### Async Flow Patterns #### Standard Matching Flow ``` create_intention(content) → publish_intention(id) → trigger_matching(id) → poll get_match_status(id) until 'completed' → get_matches(id) → present candidates to user → start_negotiations(id, user_selected_ids) ``` #### Quick Search Shortcut ``` quick_search(content) → returns matches directly (synchronous) ``` Combines create + profile + match in one call. Use when user wants fast results without managing the intention lifecycle. #### Negotiation → Order Flow ``` (in active negotiation) → business sends proposal (send_proposal with status='final_deal') → user reviews proposal → accept_deal(session_id) → creates contract + order → pay_order(order_id, currency) → (wait for delivery) → accept_delivery(order_id) → releases escrow, moves to pending_rating → submit_review(order_id, rating) ``` ## API Overview Auth: `Authorization: Bearer <TMR_API_KEY>`. All paths prefixed with `/api/v1`. UUIDs for all IDs. Bilingual fields use `_zh`/`_en` suffixes. Pagination via `offset`+`limit`. Key domains: auth, wallet, intentions, businesses, orders, contracts, credit, reviews, disputes, messages, notifications, apparatus. See `references/` for detailed request/response schemas per domain. ## Error Summary | Status | Meaning | |--------|---------| | 400 | Bad request — validation failed | | 401 | Unauthorized — invalid or missing token | | 403 | Forbidden — insufficient role/permissions | | 404 | Not found | | 409 | Conflict — duplicate or invalid state transition | | 422 | Unprocessable entity — schema validation error | | 500 | Internal server error |

path: _meta.json
content: ownerId: tmrland
slug: tmrland-personal
version: 1.6.0
publishedAt: 1740672000000

path: scripts/get-kyc.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help } = parseArgs(process.argv);
if (help) {
  console.error("Usage: get-kyc.mjs");
  process.exit(2);
}

const data = await tmrFetch("GET", "/wallet/kyc");
console.log(JSON.stringify(data, null, 2));


path: scripts/mark-negotiation-read.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: mark-negotiation-read.mjs <session-id>");
  process.exit(2);
}

await tmrFetch("POST", `/negotiations/${positional[0]}/messages/read`);
console.log(`Negotiation ${positional[0]} messages marked as read.`);


path: scripts/request-revision.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named, positional } = parseArgs(process.argv);
if (help || !positional[0] || !named.feedback) {
  console.error("Usage: request-revision.mjs <order-id> --feedback '...'");
  process.exit(2);
}

const data = await tmrFetch("POST", `/orders/${positional[0]}/request-revision`, { feedback: named.feedback });
console.log(JSON.stringify(data, null, 2));


path: scripts/get-credit-disputes.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-credit-disputes.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/credit/${positional[0]}/disputes`);
console.log(JSON.stringify(data, null, 2));


path: scripts/start-negotiation.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help || !named.intention || !named.businesses) {
  console.error("Usage: start-negotiation.mjs --intention <id> --businesses <id1,id2,...>");
  process.exit(2);
}

const body = {
  intention_id: named.intention,
  business_ids: named.businesses.split(",").map((s) => s.trim()),
};

const data = await tmrFetch("POST", "/negotiations/", body);
console.log(`Negotiation sessions created:`);
for (const s of data.sessions ?? [data]) {
  console.log(`  - ${s.id} (business: ${s.business_id})`);
}
console.log(JSON.stringify(data, null, 2));


path: scripts/get-business.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-business.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/businesses/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-agent-card.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-agent-card.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/businesses/${positional[0]}/agent-card`);
console.log(JSON.stringify(data, null, 2));


path: scripts/create-intention.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help || !named.content) {
  console.error('Usage: create-intention.mjs --content "..." [--locale zh]');
  process.exit(2);
}

const body = {
  content: named.content,
};
if (named.locale) body.locale = named.locale;

const data = await tmrFetch("POST", "/intentions/", body);
console.log(`Intention created: ${data.id}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/search-businesses.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: search-businesses.mjs [--limit N]");
  process.exit(2);
}

const limit = Number.parseInt(named.limit ?? "20", 10);
const data = await tmrFetch("GET", `/businesses/?limit=${limit}`);

console.log(`## Businesses (${data.items?.length ?? 0} of ${data.total ?? "?"})\n`);
for (const s of data.items ?? []) {
  console.log(`- **${s.brand_name_en}** (${s.brand_name_zh}) — reputation: ${s.reputation_score ?? "N/A"}`);
  console.log(`  ID: ${s.id} | Status: ${s.status}`);
  if (s.description_en) console.log(`  ${s.description_en.slice(0, 120)}`);
  console.log();
}


path: scripts/reject-deal.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: reject-deal.mjs <session-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/negotiations/${positional[0]}/reject`);
console.log(`Deal rejected for session ${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-reviews.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-reviews.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/reviews/business/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-wallet.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help } = parseArgs(process.argv);
if (help) {
  console.error("Usage: get-wallet.mjs");
  process.exit(2);
}

const data = await tmrFetch("GET", "/wallet");
console.log(`## Wallet\n`);
console.log(`USD: ${data.usd_balance ?? "0.00"}`);
console.log(`USDC: ${data.usdc_balance ?? "0.00"}`);
console.log(`\n${JSON.stringify(data, null, 2)}`);


path: scripts/charge-wallet.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help || !named.amount) {
  console.error("Usage: charge-wallet.mjs --amount N [--currency USD]");
  process.exit(2);
}

const body = { amount: Number(named.amount), currency: named.currency || "USD" };
const data = await tmrFetch("POST", `/wallet/charge`, body);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-order-reviews.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-order-reviews.mjs <order-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/reviews/order/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/list-orders.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-orders.mjs [--limit N]");
  process.exit(2);
}

const limit = Number.parseInt(named.limit ?? "20", 10);
const data = await tmrFetch("GET", `/orders/?role=personal&limit=${limit}`);

console.log(`## Orders (${data.items?.length ?? 0})\n`);
for (const o of data.items ?? []) {
  console.log(`- ${o.id} | ${o.status} | $${o.amount ?? "—"} | ${o.created_at}`);
}


path: scripts/quick-search.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help || !named.content) {
  console.error('Usage: quick-search.mjs --content "Need an NLP model for sentiment analysis"');
  process.exit(2);
}

const data = await tmrFetch("POST", "/intentions/search", { content: named.content });
console.log(JSON.stringify(data, null, 2));


path: scripts/list-conversations.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-conversations.mjs [--limit N]");
  process.exit(2);
}

const limit = named.limit || "20";
const data = await tmrFetch("GET", `/messages/conversations?limit=${limit}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/redescribe-intention.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named, positional } = parseArgs(process.argv);
if (help || !positional[0] || !named.content) {
  console.error("Usage: redescribe-intention.mjs <intention-id> --content '...' [--locale zh]");
  process.exit(2);
}

const body = { content: named.content };
if (named.locale) body.locale = named.locale;

const data = await tmrFetch("POST", `/intentions/${positional[0]}/redescribe`, body);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-question-leaderboard.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-question-leaderboard.mjs <question-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/apparatus/${positional[0]}/leaderboard`);
console.log(JSON.stringify(data, null, 2));


path: scripts/list-transactions.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-transactions.mjs [--limit N]");
  process.exit(2);
}

const limit = named.limit || "20";
const data = await tmrFetch("GET", `/wallet/transactions?limit=${limit}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/trigger-match.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || positional.length === 0) {
  console.error("Usage: trigger-match.mjs <intention-id>");
  process.exit(2);
}

const intentionId = positional[0];
const data = await tmrFetch("POST", `/intentions/${intentionId}/match`);
console.log(`Matching triggered for intention ${intentionId}`);

const candidates = data.candidates ?? data.items ?? [];
console.log(`\n## Match Candidates (${candidates.length})\n`);
for (const c of candidates) {
  console.log(`- **${c.business_name_en ?? c.business_id}** — score: ${c.score ?? "N/A"}`);
  if (c.recall_sources) console.log(`  Sources: ${c.recall_sources.join(", ")}`);
  if (c.explanation) console.log(`  ${c.explanation.slice(0, 150)}`);
  console.log();
}


path: scripts/create-dispute.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named, positional } = parseArgs(process.argv);
if (help || !positional[0] || !named.reason) {
  console.error("Usage: create-dispute.mjs <order-id> --reason '...' [--refund-type full|partial] [--refund-amount N]");
  process.exit(2);
}

const body = { reason: named.reason };
if (named["refund-type"]) body.refund_type = named["refund-type"];
if (named["refund-amount"]) body.refund_amount = Number(named["refund-amount"]);

const data = await tmrFetch("POST", `/orders/${positional[0]}/dispute`, body);
console.log(JSON.stringify(data, null, 2));


path: scripts/list-questions.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-questions.mjs [--limit N]");
  process.exit(2);
}

const limit = named.limit || "20";
const data = await tmrFetch("GET", `/apparatus/?limit=${limit}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-contract.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-contract.mjs <contract-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/contracts/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-messages.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-messages.mjs <order-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/messages/orders/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/withdraw-proposal.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: withdraw-proposal.mjs <session-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/negotiations/${positional[0]}/withdraw`);
console.log(`Proposal withdrawn for session ${positional[0]}.`);
console.log(JSON.stringify(data, null, 2));


path: scripts/pay-order.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional, named } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: pay-order.mjs <order-id> [--currency USD|USDC]");
  process.exit(2);
}

const currency = named.currency ?? "USD";
const data = await tmrFetch("POST", `/orders/${positional[0]}/pay`, { currency });
console.log(`Order ${data.id} paid (${currency}) — status: ${data.status}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/order-status.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || positional.length === 0) {
  console.error("Usage: order-status.mjs <order-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/orders/${positional[0]}`);
console.log(`## Order ${data.id}\n`);
console.log(`Status: ${data.status}`);
console.log(`Amount: $${data.amount ?? "—"}`);
console.log(`Business: ${data.business_id}`);
console.log(`Created: ${data.created_at}`);
if (data.delivered_at) console.log(`Delivered: ${data.delivered_at}`);


path: scripts/publish-intention.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: publish-intention.mjs <intention-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/intentions/${positional[0]}/publish`);
console.log(`Intention ${positional[0]} published — status: ${data.status}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/vote-answer.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named, positional } = parseArgs(process.argv);
if (help || !positional[0] || !named.direction) {
  console.error("Usage: vote-answer.mjs <answer-id> --direction like|dislike");
  process.exit(2);
}

const data = await tmrFetch("POST", `/apparatus/answers/${positional[0]}/${named.direction}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/send-message.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional, named } = parseArgs(process.argv);
if (help || !positional[0] || !named.content) {
  console.error('Usage: send-message.mjs <order-id> --content "message text"');
  process.exit(2);
}

const data = await tmrFetch("POST", `/messages/orders/${positional[0]}`, {
  content: named.content,
});
console.log(`Message sent in order ${positional[0]}: ${data.id}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/delete-intention.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: delete-intention.mjs <intention-id>");
  process.exit(2);
}

await tmrFetch("DELETE", `/intentions/${positional[0]}`);
console.log(`Intention ${positional[0]} deleted.`);


path: scripts/get-matches.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-matches.mjs <intention-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/intentions/${positional[0]}/matches`);
const candidates = data.candidates ?? data.items ?? [];
console.log(`## Match Candidates (${candidates.length})\n`);
for (const c of candidates) {
  console.log(`- **${c.business_name_en ?? c.business_id}** — score: ${c.score ?? "N/A"}`);
  if (c.recall_sources) console.log(`  Sources: ${c.recall_sources.join(", ")}`);
  if (c.explanation) console.log(`  ${c.explanation.slice(0, 150)}`);
  console.log();
}


path: scripts/submit-kyc.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help || !named.name || !named["id-type"] || !named["id-number"]) {
  console.error("Usage: submit-kyc.mjs --name '...' --id-type passport|national_id|driver_license --id-number '...'");
  process.exit(2);
}

const body = {
  name: named.name,
  id_type: named["id-type"],
  id_number: named["id-number"],
};
const data = await tmrFetch("POST", `/wallet/kyc`, body);
console.log(JSON.stringify(data, null, 2));


path: scripts/unread-count.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help } = parseArgs(process.argv);
if (help) {
  console.error("Usage: unread-count.mjs");
  process.exit(2);
}

const data = await tmrFetch("GET", "/notifications/unread-count");
console.log(JSON.stringify(data, null, 2));


path: scripts/get-leaderboard.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help } = parseArgs(process.argv);
if (help) {
  console.error("Usage: get-leaderboard.mjs");
  process.exit(2);
}

const data = await tmrFetch("GET", `/reviews/leaderboard`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-credit.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-credit.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/credit/${positional[0]}/summary`);
console.log(JSON.stringify(data, null, 2));


path: scripts/accept-delivery.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: accept-delivery.mjs <order-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/orders/${positional[0]}/accept-delivery`);
console.log(`Delivery accepted for order ${positional[0]} — status: ${data.status}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-receipt.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-receipt.mjs <order-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/orders/${positional[0]}/receipt`);
console.log(JSON.stringify(data, null, 2));


path: scripts/mark-messages-read.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: mark-messages-read.mjs <order-id>");
  process.exit(2);
}

await tmrFetch("POST", `/messages/orders/${positional[0]}/read`);
console.log(`Messages for order ${positional[0]} marked as read.`);


path: scripts/get-reputation.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-reputation.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/reviews/reputation/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/negotiation-messages.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional, named } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error('Usage: negotiation-messages.mjs <session-id> [--send "message text"]');
  process.exit(2);
}

const sessionId = positional[0];

if (named.send) {
  const msg = await tmrFetch("POST", `/negotiations/${sessionId}/messages`, {
    content: named.send,
  });
  console.log(`Message sent: ${msg.id}`);
}

const data = await tmrFetch("GET", `/negotiations/${sessionId}/messages`);
console.log(`## Messages for session ${sessionId}\n`);
for (const m of data.items ?? []) {
  const role = m.sender_role ?? "unknown";
  console.log(`[${role}] ${m.content}`);
}


path: scripts/list-notifications.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-notifications.mjs");
  process.exit(2);
}

const data = await tmrFetch("GET", `/notifications/`);
console.log(JSON.stringify(data, null, 2));


path: scripts/withdraw-wallet.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help || !named.amount) {
  console.error("Usage: withdraw-wallet.mjs --amount N [--currency USD]");
  process.exit(2);
}

const body = { amount: Number(named.amount), currency: named.currency || "USD" };
const data = await tmrFetch("POST", `/wallet/withdraw`, body);
console.log(JSON.stringify(data, null, 2));


path: scripts/match-status.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: match-status.mjs <intention-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/intentions/${positional[0]}/match/status`);
console.log(`## Match Status for ${positional[0]}\n`);
console.log(`Status: ${data.status}`);
if (data.started_at) console.log(`Started: ${data.started_at}`);
if (data.completed_at) console.log(`Completed: ${data.completed_at}`);


path: scripts/get-credit-profile.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-credit-profile.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/credit/${positional[0]}/profile`);
console.log(JSON.stringify(data, null, 2));


path: scripts/list-disputes.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-disputes.mjs [--limit N]");
  process.exit(2);
}

const limit = Number.parseInt(named.limit ?? "20", 10);
const data = await tmrFetch("GET", `/disputes/?limit=${limit}`);

console.log(`## Disputes (${data.items?.length ?? 0})\n`);
for (const d of data.items ?? []) {
  console.log(`- ${d.id} | ${d.status ?? "—"} | ${d.created_at ?? "—"}`);
}
console.log(JSON.stringify(data, null, 2));


path: scripts/get-intention.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-intention.mjs <intention-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/intentions/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/cancel-negotiation.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: cancel-negotiation.mjs <session-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/negotiations/${positional[0]}/cancel`);
console.log(`Negotiation ${positional[0]} cancelled — status: ${data.status}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/get-credit-reviews.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-credit-reviews.mjs <business-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/credit/${positional[0]}/reviews`);
console.log(JSON.stringify(data, null, 2));


path: scripts/cancel-intention.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: cancel-intention.mjs <intention-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/intentions/${positional[0]}/cancel`);
console.log(`Intention ${positional[0]} cancelled — status: ${data.status}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/list-contracts.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-contracts.mjs [--limit N]");
  process.exit(2);
}

const limit = named.limit || "20";
const data = await tmrFetch("GET", `/contracts/?limit=${limit}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/submit-review.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help || !named.order || !named.rating) {
  console.error('Usage: submit-review.mjs --order <id> --rating <1-5> [--comment "..."]');
  process.exit(2);
}

const body = { rating: Number.parseInt(named.rating, 10) };
if (named.comment) body.comment = named.comment;

const data = await tmrFetch("POST", `/reviews/order/${named.order}`, body);
console.log(`Review submitted for order ${named.order} (rating: ${body.rating})`);
console.log(JSON.stringify(data, null, 2));


path: scripts/_lib.mjs
content: /**
 * Shared HTTP helper for TMR Land personal scripts.
 * Reads TMR_API_KEY and TMR_BASE_URL from env.
 */

const [REDACTED_SECRET] ?? "").trim();
const BASE_URL = (process.env.TMR_BASE_URL ?? "https://tmrland.com/api/v1").replace(/\/$/, "");

if (!API_KEY) {
  console.error("Error: TMR_API_KEY environment variable is required.");
  process.exit(1);
}

/**
 * Make an authenticated request to the TMR Land API.
 * @param {string} method - HTTP method
 * @param {string} path - API path (e.g. "/businesses/")
 * @param {object|null} body - JSON body for POST/PATCH
 * @returns {Promise<object>} parsed JSON response
 */
export async function tmrFetch(method, path, body = null) {
  const url = `${BASE_URL}${path}`;
  const opts = {
    method,
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
  };
  if (body !== null) {
    opts.body = JSON.stringify(body);
  }
  const resp = await fetch(url, opts);
  if (!resp.ok) {
    const text = await resp.text().catch(() => "");
    console.error(`API error ${resp.status}: ${text}`);
    process.exit(1);
  }
  return resp.json();
}

/**
 * Parse --key value pairs and positional args from argv.
 */
export function parseArgs(argv) {
  const args = argv.slice(2);
  const named = {};
  const positional = [];
  for (let i = 0; i < args.length; i++) {
    if (args[i].startsWith("--")) {
      const key = args[i].slice(2);
      named[key] = args[i + 1] ?? "";
      i++;
    } else if (args[i] === "-h" || args[i] === "--help") {
      return { help: true, named, positional };
    } else {
      positional.push(args[i]);
    }
  }
  return { help: false, named, positional };
}


path: scripts/mark-notification-read.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: mark-notification-read.mjs <notification-id>");
  process.exit(2);
}

await tmrFetch("POST", `/notifications/${positional[0]}/read`);
console.log(`Notification ${positional[0]} marked as read.`);


path: scripts/list-intentions.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-intentions.mjs [--limit N]");
  process.exit(2);
}

const limit = Number.parseInt(named.limit ?? "20", 10);
const data = await tmrFetch("GET", `/intentions/?limit=${limit}`);

console.log(`## Intentions (${data.items?.length ?? 0})\n`);
for (const i of data.items ?? []) {
  console.log(`- ${i.id} | ${i.status} | ${i.content?.slice(0, 80) ?? "—"}`);
}


path: scripts/mark-all-read.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help } = parseArgs(process.argv);
if (help) {
  console.error("Usage: mark-all-read.mjs");
  process.exit(2);
}

await tmrFetch("POST", `/notifications/read-all`);
console.log("All notifications marked as read.");


path: scripts/get-negotiation.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: get-negotiation.mjs <session-id>");
  process.exit(2);
}

const data = await tmrFetch("GET", `/negotiations/${positional[0]}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/cancel-order.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: cancel-order.mjs <order-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/orders/${positional[0]}/cancel`);
console.log(`Order ${positional[0]} cancelled — status: ${data.status}`);
console.log(JSON.stringify(data, null, 2));


path: scripts/list-negotiations.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named } = parseArgs(process.argv);
if (help) {
  console.error("Usage: list-negotiations.mjs [--intention <id>]");
  process.exit(2);
}

let path = "/negotiations/?role=personal";
if (named.intention) path += `&intention_id=${named.intention}`;

const data = await tmrFetch("GET", path);
console.log(`## Negotiations (${data.items?.length ?? 0})\n`);
for (const s of data.items ?? []) {
  console.log(`- ${s.id} | status: ${s.status} | business: ${s.business_id}`);
}


path: scripts/update-intention.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, named, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: update-intention.mjs <intention-id> [--title '...'] [--description '...']");
  process.exit(2);
}

const body = {};
if (named.title) body.title = named.title;
if (named.description) body.description = named.description;

const data = await tmrFetch("PATCH", `/intentions/${positional[0]}`, body);
console.log(JSON.stringify(data, null, 2));


path: scripts/accept-deal.mjs
content: #!/usr/bin/env node
import { tmrFetch, parseArgs } from "./_lib.mjs";

const { help, positional } = parseArgs(process.argv);
if (help || !positional[0]) {
  console.error("Usage: accept-deal.mjs <session-id>");
  process.exit(2);
}

const data = await tmrFetch("POST", `/negotiations/${positional[0]}/accept`);
console.log(`Deal accepted — order created: ${data.order_id ?? data.id}`);
console.log(JSON.stringify(data, null, 2));


path: references/user-api.md
content: # User API

Base URL: `/api/v1/users`

All endpoints require authentication via Bearer token.

---

## GET /api/v1/users/me

Retrieve the current authenticated user's profile.

**Auth:** Required (any role)

### Request Body

None.

### Request Example

```
GET /api/v1/users/me
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "[REDACTED_SECRET]",
  "display_name": "张明",
  "avatar_url": "https://cdn.tmrland.com/avatars/a1b2c3d4.jpg",
  "role": "user",
  "is_active": true,
  "is_email_verified": true,
  "kyc_status": "verified",
  "locale_preference": "zh",
  "theme_preference": "dark",
  "created_at": "2026-02-27T10:30:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## PATCH /api/v1/users/me

Update the current user's profile fields. Only provided fields are updated.

**Auth:** Required (any role)

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | str \| None | No | Display name, max 100 characters |
| `avatar_url` | str \| None | No | Avatar image URL, max 500 characters |
| `locale_preference` | str \| None | No | Preferred locale (`"zh"`, `"en"`), max 10 characters |
| `theme_preference` | str \| None | No | Theme preference (`"light"`, `"dark"`, `"system"`), max 10 characters |

### Request Example

```json
{
  "display_name": "Ming Zhang",
  "locale_preference": "en",
  "theme_preference": "dark"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "[REDACTED_SECRET]",
  "display_name": "Ming Zhang",
  "avatar_url": "https://cdn.tmrland.com/avatars/a1b2c3d4.jpg",
  "role": "user",
  "is_active": true,
  "is_email_verified": true,
  "kyc_status": "verified",
  "locale_preference": "en",
  "theme_preference": "dark",
  "created_at": "2026-02-27T10:30:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Field exceeds max length or invalid type |

---

## PATCH /api/v1/users/me/password

Change the current user's password. Requires the old password for verification.

**Auth:** Required (any role)

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `old_password` | str | Yes | Current password |
| `new_password` | str | Yes | New password, minimum 8 characters |

### Request Example

```json
{
  "old_password": "securePass123",
  "new_password": "evenMoreSecure456"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "message": "Password updated successfully"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Incorrect old password"` | Old password does not match |
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | New password too short |


path: references/messages-api.md
content: # Messages API

Base URL: `/api/v1/messages`

Order-scoped messaging between personal users and businesses. Messages are tied to orders and support text content with optional file attachments.

All endpoints require authentication.

---

## GET /api/v1/messages/conversations

List all conversations (orders with messages) for the current user.

**Auth:** Required

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/messages/conversations?offset=0&limit=10
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
      "other_party_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "other_party_name": "智能数据科技",
      "last_message_content": "模型已部署完成，API文档请查看附件。",
      "last_message_at": "2026-03-10T08:45:00Z",
      "unread_count": 2,
      "total_messages": 18
    },
    {
      "order_id": "66778899-aabb-ccdd-eeff-223344556677",
      "other_party_id": "22334455-6677-8899-aabb-ccddeeff0011",
      "other_party_name": "深度语言实验室",
      "last_message_content": "好的，我们会在本周五之前提供修订版本。",
      "last_message_at": "2026-03-08T16:20:00Z",
      "unread_count": 0,
      "total_messages": 7
    }
  ],
  "total": 2
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## GET /api/v1/messages/orders/{order_id}

Retrieve the message history for an order.

**Auth:** Required (party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `order_id` | UUID | Yes | Order ID |

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `50`, max `200` |

### Request Example

```
GET /api/v1/messages/orders/55667788-99aa-bbcc-ddee-ff0011223344?offset=0&limit=50
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "ff001122-3344-5566-7788-99aabb000001",
      "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
      "sender_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "recipient_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "message_type": "text",
      "content": "你好，我们已经下单了金融大模型微调服务。请问预计多久可以开始训练？",
      "attachments": null,
      "is_read": true,
      "read_at": "2026-02-27T15:10:00Z",
      "created_at": "2026-02-27T15:05:00Z",
      "updated_at": "2026-02-27T15:10:00Z"
    },
    {
      "id": "ff001122-3344-5566-7788-99aabb000002",
      "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
      "sender_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "recipient_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "message_type": "text",
      "content": "感谢下单！我们会在确认数据范围后48小时内启动训练。请问您能提供具体的财报年份范围和需要覆盖的股票范围吗？",
      "attachments": null,
      "is_read": true,
      "read_at": "2026-02-27T15:20:00Z",
      "created_at": "2026-02-27T15:15:00Z",
      "updated_at": "2026-02-27T15:20:00Z"
    },
    {
      "id": "ff001122-3344-5566-7788-99aabb000003",
      "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
      "sender_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "recipient_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "message_type": "text",
      "content": "2021-2025年全部A股上市公司，重点关注沪深300成分股。详细需求文档见附件。",
      "attachments": [
        "https://cdn.tmrland.com/messages/55667788/requirements-v1.pdf"
      ],
      "is_read": true,
      "read_at": "2026-02-27T15:35:00Z",
      "created_at": "2026-02-27T15:30:00Z",
      "updated_at": "2026-02-27T15:35:00Z"
    }
  ],
  "total": 3
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to view messages for this order"` | User is not personal or business |
| 404 | `"Order not found"` | Order ID does not exist |

---

## POST /api/v1/messages/orders/{order_id}

Send a message in an order conversation.

**Auth:** Required (party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `order_id` | UUID | Yes | Order ID |

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | str | Yes | Message text, minimum 1 character |
| `attachments` | list[str] \| None | No | URLs to attached files |

### Request Example

```json
{
  "content": "收到需求文档，确认范围无误。训练预计3月3日启动，中间会分阶段给您进展更新。",
  "attachments": null
}
```

### Response Example

**Status: 201 Created**

```json
{
  "id": "ff001122-3344-5566-7788-99aabb000004",
  "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "sender_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "recipient_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "message_type": "text",
  "content": "收到需求文档，确认范围无误。训练预计3月3日启动，中间会分阶段给您进展更新。",
  "attachments": null,
  "is_read": false,
  "read_at": null,
  "created_at": "2026-02-28T09:00:00Z",
  "updated_at": "2026-02-28T09:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to send messages for this order"` | User is not personal or business |
| 404 | `"Order not found"` | Order ID does not exist |
| 422 | Pydantic validation array | Empty content |

---

## POST /api/v1/messages/orders/{order_id}/read

Mark all unread messages in the order conversation as read for the current user.

**Auth:** Required (party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `order_id` | UUID | Yes | Order ID |

### Request Body

None.

### Request Example

```
POST /api/v1/messages/orders/55667788-99aa-bbcc-ddee-ff0011223344/read
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "marked_read": 2
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access messages for this order"` | User is not personal or business |
| 404 | `"Order not found"` | Order ID does not exist |


path: references/wallet-api.md
content: # Wallet API

Base URL: `/api/v1/wallet`

All endpoints require authentication via Bearer token. Each user has a single wallet created automatically on registration.

---

## GET /api/v1/wallet

Retrieve the current user's wallet balances.

**Auth:** Required

### Request Body

None.

### Request Example

```
GET /api/v1/wallet
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "usd_balance": "1500.00",
  "usdc_balance": "0.00",
  "created_at": "2026-02-27T10:30:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## POST /api/v1/wallet/charge

Add funds to the wallet (simulated top-up for MVP).

**Auth:** Required

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | Decimal | Yes | Amount to add, must be > 0 and <= 100,000 |
| `currency` | str | No | Currency code, default `"USD"`. Accepted: `"USD"`, `"USDC"` |

### Request Example

```json
{
  "amount": "500.00",
  "currency": "USD"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "wallet_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "type": "charge",
  "amount": "500.00",
  "currency": "USD",
  "related_order_id": null,
  "status": "completed",
  "description": "Wallet top-up",
  "created_at": "2026-02-27T11:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Amount must be greater than 0"` | Non-positive amount |
| 400 | `"Amount exceeds maximum limit"` | Amount > 100,000 |
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Invalid amount or currency format |

---

## POST /api/v1/wallet/withdraw

Withdraw funds from the wallet (simulated withdrawal for MVP).

**Auth:** Required

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | Decimal | Yes | Amount to withdraw, must be > 0 and <= 100,000 |
| `currency` | str | No | Currency code, default `"USD"`. Accepted: `"USD"`, `"USDC"` |

### Request Example

```json
{
  "amount": "200.00",
  "currency": "USD"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "d4e5f6a7-b8c9-0123-defa-234567890123",
  "wallet_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "type": "withdraw",
  "amount": "200.00",
  "currency": "USD",
  "related_order_id": null,
  "status": "completed",
  "description": "Wallet withdrawal",
  "created_at": "2026-02-27T11:15:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Insufficient balance"` | Available balance < requested amount |
| 400 | `"Amount must be greater than 0"` | Non-positive amount |
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Invalid amount or currency format |

---

## GET /api/v1/wallet/transactions

List wallet transactions with pagination.

**Auth:** Required

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/wallet/transactions?offset=0&limit=20
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "wallet_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "type": "charge",
      "amount": "500.00",
      "currency": "USD",
      "related_order_id": null,
      "status": "completed",
      "description": "Wallet top-up",
      "created_at": "2026-02-27T11:00:00Z"
    },
    {
      "id": "e5f6a7b8-c9d0-1234-efab-345678901234",
      "wallet_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "type": "escrow_debit",
      "amount": "200.00",
      "currency": "USD",
      "related_order_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
      "status": "completed",
      "description": "Escrow debit for order f6a7b8c9",
      "created_at": "2026-02-27T12:00:00Z"
    }
  ],
  "total": 2
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Invalid offset or limit value |

---

## POST /api/v1/wallet/kyc

Submit KYC (Know Your Customer) verification information.

**Auth:** Required

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `full_name` | str | Yes | Legal full name, max 200 characters |
| `id_type` | str | Yes | Identity document type (e.g. `"passport"`, `"id_card"`, `"driver_license"`), max 50 characters |
| `id_number` | str | Yes | Identity document number, max 100 characters |

### Request Example

```json
{
  "full_name": "张明",
  "id_type": "id_card",
  "id_number": "310101199001011234"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "kyc_status": "verified",
  "kyc_data": {
    "full_name": "张明",
    "id_type": "id_card",
    "id_number": "310101****1234"
  }
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"KYC already submitted"` | KYC data already exists for this user |
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Missing or invalid fields |

---

## GET /api/v1/wallet/kyc

Retrieve the current user's KYC status and masked data.

**Auth:** Required

### Request Body

None.

### Request Example

```
GET /api/v1/wallet/kyc
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "kyc_status": "verified",
  "kyc_data": {
    "full_name": "张明",
    "id_type": "id_card",
    "id_number": "310101****1234"
  }
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 404 | `"KYC data not found"` | User has not submitted KYC |


path: references/orders-api.md
content: # Orders API

Base URL: `/api/v1/orders`

Orders represent a personal user-business transaction created through the negotiation flow. The personal user pays (choosing USD or USDC, debiting funds from wallet), reviews the delivery, accepts it (releasing funds) or requests revisions, and finally submits a rating. Businesses deliver and submit deliverables.

Order lifecycle: `pending_payment → delivering → pending_review → pending_rating → completed`, with `revision_requested` as a loop state back to `pending_review`. Orders can also enter `disputed` (via dispute) and `refunded` (after dispute resolution) states.

All endpoints require authentication. Role annotations indicate which party can perform each action.

---

## GET /api/v1/orders/

List orders for the current user with optional role and status filtering.

**Auth:** Required

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `role` | str | No | Filter by role perspective, default `"personal"`. Values: `"personal"`, `"business"` |
| `status_filter` | str \| None | No | Filter by order status (e.g., `"delivering"`, `"pending_review"`, `"pending_rating"`, `"completed"`) |
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "55667788-99aa-bbcc-ddee-ff0011223344",
      "intention_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
      "personal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "session_id": "aabb1122-3344-5566-7788-99aabbccddee",
      "contract_id": "66778899-aabb-ccdd-eeff-001122334455",
      "contract_terms": {},
      "amount": 8000.00,
      "accepted_currencies": 7500.00,
      "paid_currency": "USD",
      "paid_amount": 8000.00,
      "platform_fee": 400.00,
      "status": "delivering",
      "paid_at": "2026-02-27T15:00:00Z",
      "delivered_at": null,
      "accepted_at": null,
      "confirmed_at": null,
      "revision_count": 0,
      "revision_feedback": null,
      "delivery_notes": null,
      "attachments": null,
      "milestones": null,
      "created_at": "2026-02-27T14:30:00Z",
      "updated_at": "2026-02-27T15:00:00Z"
    }
  ],
  "total": 1
}
```

---

## GET /api/v1/orders/{order_id}

Retrieve a single order. Both the personal user and business party can access.

**Auth:** Required (party)

### Response Example

**Status: 200 OK**

```json
{
  "id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "intention_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
  "personal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "session_id": "aabb1122-3344-5566-7788-99aabbccddee",
  "contract_id": "66778899-aabb-ccdd-eeff-001122334455",
  "contract_terms": {
    "delivery_days": 14,
    "revision_rounds": 2
  },
  "amount": 8000.00,
  "accepted_currencies": 7500.00,
  "paid_currency": "USD",
  "paid_amount": 8000.00,
  "platform_fee": 400.00,
  "status": "pending_review",
  "paid_at": "2026-02-27T15:00:00Z",
  "delivered_at": "2026-03-10T09:00:00Z",
  "accepted_at": null,
  "confirmed_at": null,
  "revision_count": 0,
  "revision_feedback": null,
  "delivery_notes": "已完成模型微调，包含基线评测报告和API接口文档。",
  "attachments": [{"type": "link", "url": "https://cdn.tmrland.com/deliverables/55667788/model-v1.tar.gz"}],
  "milestones": null,
  "created_at": "2026-02-27T14:30:00Z",
  "updated_at": "2026-03-10T09:00:00Z"
}
```

---

## POST /api/v1/orders/{order_id}/pay

Pay for an order. The user chooses which currency to pay in (USD or USDC). The chosen amount is debited from the personal user's wallet for escrow.

**Auth:** Required (personal only)

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `currency` | str | Yes | `"USD"` or `"USDC"` — the order must have a price set for this currency |

### Request Example

```json
{
  "currency": "USD"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "amount": 8000.00,
  "accepted_currencies": 7500.00,
  "paid_currency": "USD",
  "paid_amount": 8000.00,
  "platform_fee": 400.00,
  "status": "delivering",
  "paid_at": "2026-02-27T15:00:00Z",
  "..."
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"This order does not have a USD price"` | Order has no `amount` but user chose USD |
| 400 | `"This order does not have a USDC price"` | Order has no `accepted_currencies` but user chose USDC |
| 400 | `"Insufficient wallet balance"` | Available balance < order amount |
| 400 | `"Order is not pending payment"` | Order status is not `pending_payment` |
| 403 | `"Not the personal account of this order"` | User is not the personal user |
| 404 | `"Order not found"` | Order ID does not exist |

---

## POST /api/v1/orders/{order_id}/accept-delivery

Accept delivery and release escrowed funds to the business. Moves order to `pending_rating`.

**Auth:** Required (personal only)

### Request Body

None.

### Response Example

**Status: 200 OK**

Returns the updated order with `status: "pending_rating"`.

---

## POST /api/v1/orders/{order_id}/request-revision

Request a revision on a pending_review order. Moves order to `revision_requested`.

**Auth:** Required (personal only)

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `feedback` | str | Yes | Revision feedback explaining what needs to change |

### Response Example

**Status: 200 OK**

Returns the updated order with `status: "revision_requested"`.

---

## POST /api/v1/orders/{order_id}/cancel

Cancel an unpaid order.

**Auth:** Required (personal only)

### Request Body

None.

---

## POST /api/v1/orders/{order_id}/dispute

Open a dispute on an order in delivering, pending_review, or revision_requested status. Triggers Agent Congress voting.

**Auth:** Required (personal)

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `reason` | str | Yes | Dispute reason, minimum 10 characters |
| `evidence` | list[str] | No | URLs to evidence files, default `[]` |
| `refund_type` | str | No | `"full"` (default) or `"partial"` |
| `requested_refund_amount` | float \| null | No | Required when `refund_type` is `"partial"`. Must be > 0 |

---

## GET /api/v1/orders/{order_id}/dispute/votes

Get Agent Congress juror votes for the order's dispute.

**Auth:** Required (party or admin)

---

## POST /api/v1/orders/{order_id}/review

Submit a review for a pending_rating order. Returns the receipt.

**Auth:** Required (personal only)

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `rating` | int | Yes | Overall rating, 1-5 |
| `would_repurchase` | bool | No | Default `false` |
| `comment` | str \| None | No | Review comment |

### Response Example

**Status: 200 OK**

```json
{
  "id": "88990011-2233-4455-6677-8899aabbccdd",
  "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "personal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "amount": 8000.00,
  "platform_fee": 400.00,
  "business_income": 7600.00,
  "paid_currency": "USD",
  "paid_amount": 8000.00,
  "cost_breakdown": {
    "total": 8000.00,
    "platform_fee": 400.00,
    "business_income": 7600.00,
    "fee_rate": 0.05
  },
  "personal_signal": {
    "rating": 5,
    "would_repurchase": true,
    "comment": "模型微调效果超出预期"
  },
  "delta_score": 0.87,
  "response_time_ms": 1423,
  "contract_id": "66778899-aabb-ccdd-eeff-001122334455",
  "created_at": "2026-03-11T10:30:00Z"
}
```

---

## GET /api/v1/orders/{order_id}/receipt

Retrieve the receipt for a completed order.

**Auth:** Required (party)

### Response Example

Same shape as the review response above.


path: references/api-keys-api.md
content: # API Keys API

Base URL: `/api/v1/api-keys`

All endpoints require authentication via Bearer token. API keys provide an alternative authentication mechanism for agent/SDK integrations, passed via the `X-API-Key` header on subsequent requests.

---

## POST /api/v1/api-keys

Create a new API key. The `raw_key` field is returned only once at creation time and cannot be retrieved again.

**Auth:** Required

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | str | Yes | Human-readable key name, max 100 characters |
| `role` | str | No | Role scope for the key, default `"personal"`. Accepted: `"personal"`, `"business"` |
| `permissions` | list[str] \| None | No | Optional list of fine-grained permission scopes |

### Request Example

```json
{
  "name": "Production Personal Agent",
  "role": "personal",
  "permissions": ["intentions:write", "orders:write", "wallet:read"]
}
```

### Response Example

**Status: 201 Created**

```json
{
  "id": "d4e5f6a7-b8c9-0123-defa-234567890123",
  "name": "Production Personal Agent",
  "key_prefix": "tmr_b_7k3m",
  "role": "personal",
  "permissions": ["intentions:write", "orders:write", "wallet:read"],
  "status": "active",
  "usage_count": 0,
  "created_at": "2026-02-27T10:30:00Z",
  "raw_key": [REDACTED_SECRET]
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Maximum API key limit reached"` | User has too many active keys |
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Name exceeds max length or invalid role |

---

## GET /api/v1/api-keys

List all API keys belonging to the current user. The `raw_key` field is never included in list responses.

**Auth:** Required

### Request Body

None.

### Request Example

```
GET /api/v1/api-keys
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
[
  {
    "id": "d4e5f6a7-b8c9-0123-defa-234567890123",
    "name": "Production Personal Agent",
    "key_prefix": "tmr_b_7k3m",
    "role": "personal",
    "permissions": ["intentions:write", "orders:write", "wallet:read"],
    "status": "active",
    "usage_count": 142,
    "created_at": "2026-02-27T10:30:00Z"
  },
  {
    "id": "e5f6a7b8-c9d0-1234-efab-345678901234",
    "name": "Staging Test Key",
    "key_prefix": "tmr_b_2n8q",
    "role": "personal",
    "permissions": null,
    "status": "active",
    "usage_count": 7,
    "created_at": "2026-02-25T08:00:00Z"
  }
]
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## DELETE /api/v1/api-keys/{key_id}

Permanently revoke and delete an API key. Only the key owner can delete their own keys.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `key_id` | UUID | Yes | ID of the API key to delete |

### Request Body

None.

### Request Example

```
DELETE /api/v1/api-keys/d4e5f6a7-b8c9-0123-defa-234567890123
[REDACTED_SECRET]
```

### Response Example

**Status: 204 No Content**

No response body.

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to delete this API key"` | User does not own this key |
| 404 | `"API key not found"` | Key ID does not exist |


path: references/apparatus-api.md
content: # Grand Apparatus API

Base URL: `/api/v1/apparatus`

The Grand Apparatus is a content and signal system where businesses answer bilingual questions (predictions, opinions, demos). It functions as both a vetting mechanism and a public content feed.

Personal users can browse questions, read answers, vote on answers, and view leaderboards. Answer submission is business-only.

---

## GET /api/v1/apparatus/

List Grand Apparatus questions with filtering and sorting.

**Auth:** None (public)

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `category` | str \| None | No | Filter by category: `"finance"`, `"politics"`, `"tech"`, `"sports"`, `"lifestyle"` |
| `status` | str \| None | No | Filter by status: `"open"`, `"closed"`, `"verified"` |
| `question_type` | str \| None | No | Filter by type: `"prediction"`, `"opinion"`, `"demo"` |
| `sort_by` | str | No | Sort order, default `"hot"`. Values: `"hot"`, `"latest"`, `"answer_count"` |
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/apparatus/?category=finance&question_type=prediction&sort_by=hot&offset=0&limit=10
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "aa110022-3344-5566-7788-99aabbccddee",
      "title_zh": "2026年Q2沪深300指数走势预测",
      "title_en": "CSI 300 Index Trend Prediction for Q2 2026",
      "description_zh": "预测2026年第二季度（4月-6月）沪深300指数的整体走势方向及目标点位区间。请给出明确的看涨/看跌/震荡判断及关键支撑/阻力位。",
      "description_en": "Predict the overall trend of the CSI 300 Index for Q2 2026 (April-June). Provide a clear bullish/bearish/sideways judgment with key support/resistance levels.",
      "question_type": "prediction",
      "category": "finance",
      "status": "open",
      "verification_deadline": "2026-07-01T00:00:00Z",
      "actual_result": null,
      "ai_generated": false,
      "quality_score": 9.2,
      "hot_score": 156.8,
      "answer_count": 12,
      "bullish_count": 7,
      "bearish_count": 3,
      "neutral_count": 2,
      "created_at": "2026-02-15T10:00:00Z",
      "updated_at": "2026-02-27T08:00:00Z"
    },
    {
      "id": "bb220033-4455-6677-8899-aabbccddeeff",
      "title_zh": "央行2026年是否会降准？",
      "title_en": "Will the PBOC Cut RRR in 2026?",
      "description_zh": "预测中国人民银行在2026年是否会进行存款准备金率下调，以及可能的时间窗口和降幅。",
      "description_en": "Predict whether the People's Bank of China will cut the Reserve Requirement Ratio in 2026, including possible timing and magnitude.",
      "question_type": "prediction",
      "category": "finance",
      "status": "open",
      "verification_deadline": "2026-12-31T00:00:00Z",
      "actual_result": null,
      "ai_generated": true,
      "quality_score": 8.5,
      "hot_score": 132.4,
      "answer_count": 8,
      "bullish_count": 6,
      "bearish_count": 1,
      "neutral_count": 1,
      "created_at": "2026-02-20T14:00:00Z",
      "updated_at": "2026-02-26T16:00:00Z"
    }
  ],
  "total": 2
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 422 | Pydantic validation array | Invalid category, question_type, or sort_by value |

---

## GET /api/v1/apparatus/{question_id}

Retrieve a single Grand Apparatus question.

**Auth:** None (public)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `question_id` | UUID | Yes | Question ID |

### Request Body

None.

### Request Example

```
GET /api/v1/apparatus/aa110022-3344-5566-7788-99aabbccddee
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "aa110022-3344-5566-7788-99aabbccddee",
  "title_zh": "2026年Q2沪深300指数走势预测",
  "title_en": "CSI 300 Index Trend Prediction for Q2 2026",
  "description_zh": "预测2026年第二季度（4月-6月）沪深300指数的整体走势方向及目标点位区间。请给出明确的看涨/看跌/震荡判断及关键支撑/阻力位。",
  "description_en": "Predict the overall trend of the CSI 300 Index for Q2 2026 (April-June). Provide a clear bullish/bearish/sideways judgment with key support/resistance levels.",
  "question_type": "prediction",
  "category": "finance",
  "status": "open",
  "verification_deadline": "2026-07-01T00:00:00Z",
  "actual_result": null,
  "ai_generated": false,
  "quality_score": 9.2,
  "hot_score": 156.8,
  "answer_count": 12,
  "bullish_count": 7,
  "bearish_count": 3,
  "neutral_count": 2,
  "created_at": "2026-02-15T10:00:00Z",
  "updated_at": "2026-02-27T08:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"Question not found"` | Question ID does not exist |

---

## GET /api/v1/apparatus/{question_id}/answers

List answers for a Grand Apparatus question.

**Auth:** None (public)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `question_id` | UUID | Yes | Question ID |

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `50`, max `200` |

### Request Example

```
GET /api/v1/apparatus/aa110022-3344-5566-7788-99aabbccddee/answers?offset=0&limit=10
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "cc330044-5566-7788-99aa-bbccddeeff00",
      "question_id": "aa110022-3344-5566-7788-99aabbccddee",
      "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "answer_text_zh": "看涨。基于我们自研的多因子模型分析，Q2流动性宽松预期明确，外资回流趋势显著，沪深300目标区间4200-4600。关键支撑位3950，阻力位4650。核心驱动因素：1) 降准降息预期；2) 科技板块盈利改善；3) 北向资金持续净流入。",
      "answer_text_en": "Bullish. Based on our proprietary multi-factor model, Q2 liquidity easing expectations are clear, with significant foreign capital inflow trends. CSI 300 target range 4200-4600. Key support at 3950, resistance at 4650. Core drivers: 1) RRR/rate cut expectations; 2) Tech sector earnings improvement; 3) Sustained northbound capital net inflows.",
      "prediction_direction": "bullish",
      "prediction_value": 4400.0,
      "prediction_window": "2026-Q2",
      "is_correct": null,
      "attribution_score": 8.7,
      "likes_count": 45,
      "dislikes_count": 3,
      "demo_url": null,
      "demo_screenshot_url": null,
      "created_at": "2026-02-16T11:00:00Z",
      "updated_at": "2026-02-27T08:00:00Z"
    },
    {
      "id": "dd440055-6677-8899-aabb-ccddeeff0011",
      "question_id": "aa110022-3344-5566-7788-99aabbccddee",
      "business_id": "22334455-6677-8899-aabb-ccddeeff0011",
      "answer_text_zh": "震荡偏多。NLP情绪指标显示市场恐慌情绪已降至低位，但政策催化尚不明朗。预计沪深300在3800-4300区间震荡，突破需要政策面明确信号。",
      "answer_text_en": "Sideways with upward bias. NLP sentiment indicators show market panic has subsided, but policy catalysts remain unclear. CSI 300 expected to range between 3800-4300, with breakout requiring clear policy signals.",
      "prediction_direction": "neutral",
      "prediction_value": 4050.0,
      "prediction_window": "2026-Q2",
      "is_correct": null,
      "attribution_score": 7.4,
      "likes_count": 28,
      "dislikes_count": 5,
      "demo_url": null,
      "demo_screenshot_url": null,
      "created_at": "2026-02-17T09:30:00Z",
      "updated_at": "2026-02-27T08:00:00Z"
    }
  ],
  "total": 2
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"Question not found"` | Question ID does not exist |

---

## POST /api/v1/apparatus/answers/{answer_id}/like

Like an answer. Toggles the like if already liked.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `answer_id` | UUID | Yes | Answer ID |

### Request Body

None.

### Request Example

```
POST /api/v1/apparatus/answers/cc330044-5566-7788-99aa-bbccddeeff00/like
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "cc330044-5566-7788-99aa-bbccddeeff00",
  "question_id": "aa110022-3344-5566-7788-99aabbccddee",
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "answer_text_zh": "看涨。基于我们自研的多因子模型分析...",
  "answer_text_en": "Bullish. Based on our proprietary multi-factor model...",
  "prediction_direction": "bullish",
  "prediction_value": 4400.0,
  "prediction_window": "2026-Q2",
  "is_correct": null,
  "attribution_score": 8.7,
  "likes_count": 46,
  "dislikes_count": 3,
  "demo_url": null,
  "demo_screenshot_url": null,
  "created_at": "2026-02-16T11:00:00Z",
  "updated_at": "2026-02-27T10:30:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 404 | `"Answer not found"` | Answer ID does not exist |

---

## POST /api/v1/apparatus/answers/{answer_id}/dislike

Dislike an answer. Toggles the dislike if already disliked.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `answer_id` | UUID | Yes | Answer ID |

### Request Body

None.

### Request Example

```
POST /api/v1/apparatus/answers/dd440055-6677-8899-aabb-ccddeeff0011/dislike
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "dd440055-6677-8899-aabb-ccddeeff0011",
  "question_id": "aa110022-3344-5566-7788-99aabbccddee",
  "business_id": "22334455-6677-8899-aabb-ccddeeff0011",
  "answer_text_zh": "震荡偏多。NLP情绪指标显示市场恐慌情绪已降至低位...",
  "answer_text_en": "Sideways with upward bias. NLP sentiment indicators show...",
  "prediction_direction": "neutral",
  "prediction_value": 4050.0,
  "prediction_window": "2026-Q2",
  "is_correct": null,
  "attribution_score": 7.4,
  "likes_count": 28,
  "dislikes_count": 6,
  "demo_url": null,
  "demo_screenshot_url": null,
  "created_at": "2026-02-17T09:30:00Z",
  "updated_at": "2026-02-27T10:35:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 404 | `"Answer not found"` | Answer ID does not exist |

---

## GET /api/v1/apparatus/{question_id}/leaderboard

Retrieve the prediction accuracy leaderboard for a specific question.

**Auth:** None (public)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `question_id` | UUID | Yes | Question ID |

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `limit` | int | No | Number of entries to return, default `20`, max `100` |

### Request Example

```
GET /api/v1/apparatus/aa110022-3344-5566-7788-99aabbccddee/leaderboard?limit=5
```

### Response Example

**Status: 200 OK**

```json
[
  {
    "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
    "accuracy_rate": 0.82,
    "total_predictions": 34,
    "correct_predictions": 28
  },
  {
    "business_id": "22334455-6677-8899-aabb-ccddeeff0011",
    "accuracy_rate": 0.75,
    "total_predictions": 28,
    "correct_predictions": 21
  },
  {
    "business_id": "33445566-7788-99aa-bbcc-ddeeff001122",
    "accuracy_rate": 0.71,
    "total_predictions": 21,
    "correct_predictions": 15
  }
]
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"Question not found"` | Question ID does not exist |
| 422 | Pydantic validation array | Invalid limit value |


path: references/credit-api.md
content: # Credit API

Base URL: `/api/v1/credit`

Business credit scoring endpoints. Provides multi-dimensional credit profiles computed from order history, reviews, and dispute records. Two views: a human-friendly summary with radar chart data, and an agent-friendly profile with raw stats for autonomous decision-making.

---

## GET /api/v1/credit/{business_id}/summary

Human-friendly credit summary with radar chart data and highlights. No authentication required.

**Auth:** None

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Response Example

```json
{
  "business_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "tier": "gold",
  "overall_score": 82.5,
  "radar": {
    "quality": 88.0,
    "speed": 75.0,
    "consistency": 85.0,
    "reputation": 80.0,
    "expertise": 84.0
  },
  "highlights": [
    "Top 10% quality rating",
    "15 completed orders with zero disputes",
    "Average delivery 1.2 days ahead of schedule"
  ],
  "total_orders": 15,
  "member_since": "2026-01-15T08:00:00Z"
}
```

### Credit Radar Dimensions

| Dimension | Weight | Description |
|---|---|---|
| `quality` | 40% | Derived from review quality_rating and overall_rating |
| `speed` | 20% | Derived from review speed_rating and delivery timeliness |
| `consistency` | 15% | Standard deviation of ratings (lower = more consistent) |
| `reputation` | 15% | Based on review volume, repurchase rate, and helpful votes |
| `expertise` | 10% | Grand Apparatus participation and prediction accuracy |

### Tiers

| Tier | Score Range |
|---|---|
| `platinum` | 90–100 |
| `gold` | 75–89 |
| `silver` | 60–74 |
| `bronze` | 40–59 |
| `unrated` | < 40 or insufficient data |

### Errors

| Status | Code | Description |
|---|---|---|
| 404 | `business_not_found` | No business with this ID |

---

## GET /api/v1/credit/{business_id}/profile

Agent-friendly credit profile with raw stats for autonomous decision-making.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Response Example

```json
{
  "business_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "credit_vector": [0.88, 0.75, 0.85, 0.80, 0.84],
  "raw_stats": {
    "total_orders": 15,
    "completed_orders": 14,
    "disputed_orders": 1,
    "avg_overall_rating": 4.4,
    "avg_quality_rating": 4.6,
    "avg_speed_rating": 3.8,
    "repurchase_rate": 0.73,
    "dispute_rate": 0.067,
    "apparatus_answers": 8,
    "prediction_accuracy": 0.625
  },
  "sample_size": 14,
  "last_updated": "2026-03-01T12:00:00Z"
}
```

### Credit Vector

The `credit_vector` is a 5-element array matching the radar dimensions in order: `[quality, speed, consistency, reputation, expertise]`. Each value is a float between 0.0 and 1.0. Agents can use this vector directly for comparison and ranking.

### Errors

| Status | Code | Description |
|---|---|---|
| 401 | `not_authenticated` | Missing or invalid access token |
| 404 | `business_not_found` | No business with this ID |

---

## GET /api/v1/credit/{business_id}/reviews

Raw review stream for agent deep-dive analysis.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset. Default `0` |
| `limit` | int | No | Number of results. Default `50` |

### Response Example

```json
{
  "items": [
    {
      "id": "rev-11223344-5566-7788-99aa-bbccddeeff00",
      "order_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "overall_rating": 5,
      "quality_rating": 5,
      "speed_rating": 4,
      "comment": "Excellent analysis with actionable insights.",
      "would_repurchase": true,
      "created_at": "2026-02-28T16:00:00Z"
    },
    {
      "id": "rev-22334455-6677-8899-aabb-ccddeeff0011",
      "order_id": "22334455-6677-8899-aabb-ccddeeff0011",
      "overall_rating": 3,
      "quality_rating": 3,
      "speed_rating": 2,
      "comment": "Delivered late but content quality was acceptable.",
      "would_repurchase": false,
      "created_at": "2026-02-20T10:00:00Z"
    }
  ],
  "total": 14
}
```

### Errors

| Status | Code | Description |
|---|---|---|
| 401 | `not_authenticated` | Missing or invalid access token |

---

## GET /api/v1/credit/{business_id}/disputes

Dispute history with outcomes for agent risk assessment.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Response Example

```json
{
  "items": [
    {
      "id": "disp-99887766-5544-3322-1100-ffeeddccbbaa",
      "order_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "reason": "Delivery did not match the agreed scope in the contract.",
      "status": "resolved",
      "resolution": "partial_refund",
      "resolution_notes": "50% refund issued due to incomplete deliverables.",
      "resolved_at": "2026-02-25T12:00:00Z",
      "created_at": "2026-02-22T08:00:00Z"
    }
  ],
  "total": 1
}
```

### Errors

| Status | Code | Description |
|---|---|---|
| 401 | `not_authenticated` | Missing or invalid access token |


path: references/businesses-api.md
content: # Businesses API

Base URL: `/api/v1/businesses`

Personal-facing read-only endpoints for browsing the business marketplace, viewing business profiles, and retrieving agent cards for A2A integration.

---

## GET /api/v1/businesses/

List all active businesses with pagination.

**Auth:** None (public)

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/businesses/?offset=0&limit=10
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "11223344-5566-7788-99aa-bbccddeeff00",
      "user_id": "aa112233-4455-6677-8899-aabbccddeeff",
      "brand_name_zh": "智能数据科技",
      "brand_name_en": "SmartData Tech",
      "logo_url": "https://cdn.tmrland.com/logos/smartdata.png",
      "description_zh": "专注于金融行业大模型微调与数据分析服务，拥有10年以上A股数据处理经验。",
      "description_en": "Specializing in financial LLM fine-tuning and data analytics with 10+ years of A-share data processing experience.",
      "reputation_score": 4.7,
      "grand_apparatus_stats": {
        "total_answers": 42,
        "accuracy_rate": 0.76,
        "prediction_score": 8.2
      },
      "status": "active",
      "created_at": "2026-01-15T08:00:00Z"
    },
    {
      "id": "22334455-6677-8899-aabb-ccddeeff0011",
      "user_id": "bb223344-5566-7788-99aa-aabbccddeeff",
      "brand_name_zh": "深度语言实验室",
      "brand_name_en": "DeepLang Lab",
      "logo_url": "https://cdn.tmrland.com/logos/deeplang.png",
      "description_zh": "多语言NLP与大模型服务商，提供定制化模型训练、部署和持续优化。",
      "description_en": "Multilingual NLP and LLM service provider offering custom model training, deployment, and continuous optimization.",
      "reputation_score": 4.3,
      "grand_apparatus_stats": {
        "total_answers": 28,
        "accuracy_rate": 0.71,
        "prediction_score": 7.5
      },
      "status": "active",
      "created_at": "2026-01-20T09:30:00Z"
    },
    {
      "id": "33445566-7788-99aa-bbcc-ddeeff001122",
      "user_id": "cc334455-6677-8899-aabb-bbccddeeff00",
      "brand_name_zh": "锐思研报",
      "brand_name_en": "RuiSi Research",
      "logo_url": "https://cdn.tmrland.com/logos/ruisi.png",
      "description_zh": "AI驱动的投研报告生成平台，覆盖A股、港股、美股三大市场。",
      "description_en": "AI-driven investment research report generation platform covering A-share, HK, and US markets.",
      "reputation_score": 4.1,
      "grand_apparatus_stats": null,
      "status": "active",
      "created_at": "2026-02-01T14:00:00Z"
    }
  ],
  "total": 3
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 422 | Pydantic validation array | Invalid offset or limit value |

---

## GET /api/v1/businesses/{business_id}

Retrieve a single business's full profile.

**Auth:** None (public)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Request Body

None.

### Request Example

```
GET /api/v1/businesses/11223344-5566-7788-99aa-bbccddeeff00
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "11223344-5566-7788-99aa-bbccddeeff00",
  "user_id": "aa112233-4455-6677-8899-aabbccddeeff",
  "brand_name_zh": "智能数据科技",
  "brand_name_en": "SmartData Tech",
  "logo_url": "https://cdn.tmrland.com/logos/smartdata.png",
  "description_zh": "专注于金融行业大模型微调与数据分析服务，拥有10年以上A股数据处理经验。",
  "description_en": "Specializing in financial LLM fine-tuning and data analytics with 10+ years of A-share data processing experience.",
  "reputation_score": 4.7,
  "grand_apparatus_stats": {
    "total_answers": 42,
    "accuracy_rate": 0.76,
    "prediction_score": 8.2
  },
  "status": "active",
  "created_at": "2026-01-15T08:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"Business not found"` | Business ID does not exist |

---

## GET /api/v1/businesses/{business_id}/agent-card

Retrieve a business's agent card, which describes their A2A (Agent-to-Agent) integration capabilities, pricing, and SLA.

**Auth:** None (public)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Request Body

None.

### Request Example

```
GET /api/v1/businesses/11223344-5566-7788-99aa-bbccddeeff00/agent-card
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "e1f2a3b4-c5d6-7890-efab-123456789abc",
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "capabilities": [
    "llm-fine-tuning",
    "financial-data-analysis",
    "report-generation",
    "real-time-market-analysis"
  ],
  "pricing": {
    "base_price": 500.0,
    "price_min": 200.0,
    "price_max": 1000.0,
    "accepted_currencies": ["USD", "USDC"]
  },
  "accepted_payment_methods": ["escrow_usd", "escrow_usdc"],
  "sla": {
    "response_time_hours": 2,
    "delivery_time_days": 14,
    "revision_rounds": 2,
    "uptime_guarantee": "99.5%"
  },
  "a2a_endpoint": "https://agents.smartdata-tech.com/v1/a2a",
  "created_at": "2026-01-15T08:30:00Z",
  "updated_at": "2026-02-20T16:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"Business not found"` | Business ID does not exist |
| 404 | `"Agent card not found"` | Business has not configured an agent card |


path: references/intentions-api.md
content: # Intentions API

Base URL: `/api/v1/intentions`

Intentions represent a personal user's structured need. The personal user creates an intention, publishes it, and then triggers matching to find business candidates via multi-path recall (rules + Elasticsearch BM25 + Milvus vector + RRF fusion).

All endpoints require authentication. Personal users have full CRUD access plus match triggering.

---

## POST /api/v1/intentions/

Create a new intention in `draft` status.

**Auth:** Required

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | str | Yes | Intention content, 2-5000 characters. Describes what you need. |
| `locale` | str | No | Locale for LLM processing, default `"zh"` |

### Request Example

```json
{
  "content": "我们需要一个针对中国A股市场的大语言模型微调服务，要求支持实时行情分析和研报生成，训练数据需覆盖最近5年的财报和公告数据。",
  "locale": "zh"
}
```

### Response Example

**Status: 201 Created**

```json
{
  "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
  "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "content": "我们需要一个针对中国A股市场的大语言模型微调服务，要求支持实时行情分析和研报生成，训练数据需覆盖最近5年的财报和公告数据。",
  "title": null,
  "description": null,
  "category": "other",
  "budget_min": 5000.00,
  "budget_max": 15000.00,
  "currency": "USD",
  "status": "draft",
  "tags": ["金融", "大模型", "微调", "A股"],
  "locale": "zh",
  "match_count": 0,
  "created_at": "2026-02-27T10:30:00Z",
  "updated_at": "2026-02-27T10:30:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Content too short/long |

---

## GET /api/v1/intentions/

List intentions belonging to the current user.

**Auth:** Required

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/intentions/?offset=0&limit=10
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
      "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "title": "需要金融行业大模型微调服务",
      "description": "我们需要一个针对中国A股市场的大语言模型微调服务...",
      "category": "model",
      "budget_min": 5000.00,
      "budget_max": 15000.00,
      "currency": "USD",
      "status": "published",
      "tags": ["金融", "大模型", "微调", "A股"],
      "locale": "zh",
      "match_count": 3,
      "created_at": "2026-02-27T10:30:00Z",
      "updated_at": "2026-02-27T11:00:00Z"
    }
  ],
  "total": 1
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## GET /api/v1/intentions/{intention_id}

Retrieve a single intention by ID. Only the owner can access their intentions.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID |

### Request Body

None.

### Request Example

```
GET /api/v1/intentions/f6a7b8c9-d0e1-2345-fabc-456789012345
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
  "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "需要金融行业大模型微调服务",
  "description": "我们需要一个针对中国A股市场的大语言模型微调服务，要求支持实时行情分析和研报生成，训练数据需覆盖最近5年的财报和公告数据。",
  "category": "model",
  "budget_min": 5000.00,
  "budget_max": 15000.00,
  "currency": "USD",
  "status": "published",
  "tags": ["金融", "大模型", "微调", "A股"],
  "locale": "zh",
  "match_count": 3,
  "created_at": "2026-02-27T10:30:00Z",
  "updated_at": "2026-02-27T11:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |

---

## PATCH /api/v1/intentions/{intention_id}

Update a draft intention. Only draft intentions can be edited.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID |

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | str \| None | No | Updated content, 2-5000 characters |

### Request Example

```json
{
  "content": "Updated description of what I need"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
  "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "需要金融行业大模型微调服务",
  "description": "我们需要一个针对中国A股市场的大语言模型微调服务，要求支持实时行情分析和研报生成，训练数据需覆盖最近5年的财报和公告数据。",
  "category": "model",
  "budget_min": 5000.00,
  "budget_max": 20000.00,
  "currency": "USD",
  "status": "draft",
  "tags": ["金融", "大模型", "微调", "A股", "研报生成"],
  "locale": "zh",
  "match_count": 0,
  "created_at": "2026-02-27T10:30:00Z",
  "updated_at": "2026-02-27T11:30:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |
| 409 | `"Can only edit intentions in draft status"` | Intention is not in draft |
| 422 | Pydantic validation array | Invalid field values |

---

## POST /api/v1/intentions/{intention_id}/publish

Transition an intention from `draft` to `published`, making it eligible for matching.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID |

### Request Body

None.

### Request Example

```
POST /api/v1/intentions/f6a7b8c9-d0e1-2345-fabc-456789012345/publish
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
  "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "需要金融行业大模型微调服务",
  "description": "我们需要一个针对中国A股市场的大语言模型微调服务，要求支持实时行情分析和研报生成，训练数据需覆盖最近5年的财报和公告数据。",
  "category": "model",
  "budget_min": 5000.00,
  "budget_max": 20000.00,
  "currency": "USD",
  "status": "published",
  "tags": ["金融", "大模型", "微调", "A股", "研报生成"],
  "locale": "zh",
  "match_count": 0,
  "created_at": "2026-02-27T10:30:00Z",
  "updated_at": "2026-02-27T12:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |
| 409 | `"Can only publish intentions in draft status"` | Intention is not in draft |

---

## POST /api/v1/intentions/{intention_id}/cancel

Cancel a published or draft intention.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID |

### Request Body

None.

### Request Example

```
POST /api/v1/intentions/f6a7b8c9-d0e1-2345-fabc-456789012345/cancel
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
  "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "需要金融行业大模型微调服务",
  "description": "我们需要一个针对中国A股市场的大语言模型微调服务...",
  "category": "model",
  "budget_min": 5000.00,
  "budget_max": 20000.00,
  "currency": "USD",
  "status": "cancelled",
  "tags": ["金融", "大模型", "微调", "A股", "研报生成"],
  "locale": "zh",
  "match_count": 3,
  "created_at": "2026-02-27T10:30:00Z",
  "updated_at": "2026-02-27T13:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |
| 409 | `"Cannot cancel intention in current status"` | Intention already cancelled or completed |

---

## DELETE /api/v1/intentions/{intention_id}

Hard-delete an intention and all associated data (profile, candidates, negotiation sessions). Active negotiation sessions are automatically cancelled before deletion. Blocked for intentions with `contracted` status.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID |

### Request Body

None.

### Request Example

```
DELETE /api/v1/intentions/f6a7b8c9-d0e1-2345-fabc-456789012345
[REDACTED_SECRET]
```

### Response

**Status: 204 No Content**

No response body.

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not your intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |
| 409 | `"Cannot delete an intention with orders"` | Intention has `contracted` status |

---

## POST /api/v1/intentions/{intention_id}/match

Trigger business matching for a published intention. Uses multi-path recall: rule-based filtering, Elasticsearch BM25 keyword search, Milvus vector similarity, and RRF fusion ranking.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID (must be in `published` status) |

### Request Body

None.

### Request Example

```
POST /api/v1/intentions/f6a7b8c9-d0e1-2345-fabc-456789012345/match
[REDACTED_SECRET]
```

### Response Example

**Status: 202 Accepted**

```json
{
  "task_id": "abc123-task-id",
  "intention_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
  "status": "dispatched"
}
```

Poll `GET /api/v1/intentions/{intention_id}/match/status` to check matching progress.

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |
| 409 | `"Can only match published intentions"` | Intention is not in `published` status |

---

## GET /api/v1/intentions/{intention_id}/match/status

Poll matching progress after triggering `POST /match`.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID |

### Request Body

None.

### Response Example

**Status: 200 OK**

```json
{
  "status": "completed",
  "intention": {
    "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
    "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "content": "我们需要一个针对中国A股市场的大语言模型微调服务...",
    "title": null,
    "description": null,
    "category": "other",
    "budget_min": 5000.00,
    "budget_max": 20000.00,
    "currency": "USD",
    "status": "matched",
    "tags": ["金融", "大模型", "微调", "A股"],
    "locale": "zh",
    "match_count": 3,
    "created_at": "2026-02-27T10:30:00Z",
    "updated_at": "2026-02-27T14:00:00Z"
  },
  "profile": {
    "id": "a7b8c9d0-e1f2-3456-abcd-567890123456",
    "intention_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
    "domain_tags": ["金融科技", "资本市场"],
    "required_capabilities": ["大模型微调", "行情分析", "研报生成"],
    "complexity_score": 0.8,
    "urgency_score": 0.5,
    "semantic_summary": "金融大模型微调，A股市场行情分析与研报生成",
    "created_at": "2026-02-27T14:00:00Z"
  },
  "candidates": [
    {
      "id": "b8c9d0e1-f2a3-4567-bcde-678901234567",
      "intention_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
      "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "rank": 1,
      "score": 0.92,
      "recall_sources": ["bm25", "vector", "rule"],
      "explanation": "该商家专注于金融大模型微调，拥有丰富的A股数据处理经验。",
      "business_name_zh": "智能数据科技",
      "business_name_en": "SmartData Tech",
      "business_reputation": 4.7
    }
  ],
  "error": null
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |

---

## POST /api/v1/intentions/search

One-shot quick search: creates an intention, generates an NLP profile, matches businesses, and returns results synchronously.

**Auth:** Required

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | str | Yes | What you need, 2-5000 characters |
| `locale` | str | No | Locale, default `"zh"` |

### Request Example

```json
{
  "content": "我需要一个AI客服解决方案，支持中英文多轮对话",
  "locale": "zh"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "intention": {
    "id": "...",
    "user_id": "...",
    "content": "我需要一个AI客服解决方案，支持中英文多轮对话",
    "title": null,
    "description": null,
    "category": "other",
    "budget_min": null,
    "budget_max": 10000.00,
    "currency": "USD",
    "status": "published",
    "tags": null,
    "locale": "zh",
    "match_count": 2,
    "created_at": "2026-02-27T14:00:00Z",
    "updated_at": "2026-02-27T14:00:00Z"
  },
  "candidates": [
    {
      "id": "...",
      "intention_id": "...",
      "business_id": "...",
      "rank": 1,
      "score": 0.88,
      "recall_sources": ["bm25", "vector"],
      "explanation": "专注于多语言AI客服方案",
      "business_name_zh": "智能客服科技",
      "business_name_en": "SmartCS Tech",
      "business_reputation": 4.5
    }
  ]
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 422 | Pydantic validation array | Content too short/long |

---

## POST /api/v1/intentions/search/stream

Same as `POST /search` but returns results as Server-Sent Events (SSE) for streaming UI updates.

**Auth:** Required

### Request Body

Same as `POST /intentions/search`.

### Response

SSE stream with events: `intention_created`, `profile_generated`, `candidates_found`, `done`, `error`.

---

## GET /api/v1/intentions/{intention_id}/matches

Retrieve saved match candidates for an intention.

**Auth:** Required (owner)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | Intention ID |

### Request Body

None.

### Request Example

```
GET /api/v1/intentions/f6a7b8c9-d0e1-2345-fabc-456789012345/matches
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
[
  {
    "id": "b8c9d0e1-f2a3-4567-bcde-678901234567",
    "intention_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
    "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
    "rank": 1,
    "score": 0.92,
    "recall_sources": ["bm25", "vector", "rule"],
    "explanation": "该商家专注于金融大模型微调，拥有丰富的A股数据处理经验，预算匹配度高。",
    "business_name_zh": "智能数据科技",
    "business_name_en": "SmartData Tech",
    "business_reputation": 4.7
  },
  {
    "id": "c9d0e1f2-a3b4-5678-cdef-789012345678",
    "intention_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
    "business_id": "22334455-6677-8899-aabb-ccddeeff0011",
    "rank": 2,
    "score": 0.85,
    "recall_sources": ["bm25", "vector"],
    "explanation": "提供通用大模型微调服务，有金融行业案例，但专注度略低于首选。",
    "business_name_zh": "深度语言实验室",
    "business_name_en": "DeepLang Lab",
    "business_reputation": 4.3
  }
]
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this intention"` | User is not the owner |
| 404 | `"Intention not found"` | ID does not exist |


path: references/notifications-api.md
content: # Notifications API

Base URL: `/api/v1/notifications`

Platform notifications for order updates, match results, dispute activity, and system announcements.

All endpoints require authentication.

---

## GET /api/v1/notifications/

List notifications for the current user with optional filtering.

**Auth:** Required

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `unread_only` | bool | No | If `true`, only return unread notifications. Default `false` |
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/notifications/?unread_only=true&offset=0&limit=10
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "11002233-4455-6677-8899-aabbccddeeff",
      "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "type": "order_delivered",
      "title": "订单交付通知",
      "body": "商家「智能数据科技」已完成订单交付，请查看交付物并确认收货。",
      "resource_type": "order",
      "resource_id": "55667788-99aa-bbcc-ddee-ff0011223344",
      "is_read": false,
      "read_at": null,
      "created_at": "2026-03-10T09:00:00Z"
    },
    {
      "id": "22003344-5566-7788-99aa-bbccddeeff00",
      "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "type": "match_completed",
      "title": "匹配完成",
      "body": "您的需求「需要金融行业大模型微调服务」已完成商家匹配，共找到3个候选商家。",
      "resource_type": "intention",
      "resource_id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
      "is_read": false,
      "read_at": null,
      "created_at": "2026-02-27T14:00:00Z"
    },
    {
      "id": "33004455-6677-8899-aabb-ccddeeff0011",
      "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "type": "dispute_message",
      "title": "争议回复",
      "body": "商家在争议 #77889900 中提交了新回复。",
      "resource_type": "dispute",
      "resource_id": "77889900-aabb-ccdd-eeff-112233445566",
      "is_read": false,
      "read_at": null,
      "created_at": "2026-03-12T10:30:00Z"
    }
  ],
  "total": 3
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## GET /api/v1/notifications/unread-count

Get the count of unread notifications for the current user.

**Auth:** Required

### Request Body

None.

### Request Example

```
GET /api/v1/notifications/unread-count
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "count": 3
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## POST /api/v1/notifications/{notification_id}/read

Mark a single notification as read.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `notification_id` | UUID | Yes | Notification ID |

### Request Body

None.

### Request Example

```
POST /api/v1/notifications/11002233-4455-6677-8899-aabbccddeeff/read
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "11002233-4455-6677-8899-aabbccddeeff",
  "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "order_delivered",
  "title": "订单交付通知",
  "body": "商家「智能数据科技」已完成订单交付，请查看交付物并确认收货。",
  "resource_type": "order",
  "resource_id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "is_read": true,
  "read_at": "2026-03-10T09:30:00Z",
  "created_at": "2026-03-10T09:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 404 | `"Notification not found"` | Notification ID does not exist or belongs to another user |

---

## POST /api/v1/notifications/read-all

Mark all unread notifications as read for the current user.

**Auth:** Required

### Request Body

None.

### Request Example

```
POST /api/v1/notifications/read-all
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "marked_read": 3
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |


path: references/reviews-api.md
content: # Reviews API

Base URL: `/api/v1/reviews`

The reviews system captures personal feedback on orders and aggregates business reputation scores. Reviews are submitted during the `pending_rating` phase. One review per order.

---

## POST /api/v1/orders/{order_id}/review

Submit a review for an order in `pending_rating` status. Transitions the order to `completed`.

**Auth:** Required (personal only)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `order_id` | UUID | Yes | Order ID (must be in `pending_rating` status) |

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `rating` | int | Yes | Overall rating, 1-5 |
| `comment` | str \| None | No | Review comment text |
| `would_repurchase` | bool | No | Whether the personal user would buy again, default `false` |

### Request Example

```json
{
  "rating": 5,
  "comment": "智能数据科技团队对A股数据的理解非常深入，模型在研报生成任务上的表现远超我们的预期。交付速度略慢于预期，但最终质量非常高。",
  "would_repurchase": true
}
```

### Response Example

**Status: 201 Created**

```json
{
  "id": "aabb0011-2233-4455-6677-889900112233",
  "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "status": "completed",
  "rating": 5,
  "comment": "智能数据科技团队对A股数据的理解非常深入，模型在研报生成任务上的表现远超我们的预期。交付速度略慢于预期，但最终质量非常高。",
  "would_repurchase": true,
  "created_at": "2026-03-11T11:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Only the personal user can review this order"` | User is not the personal user |
| 404 | `"Order not found"` | Order ID does not exist |
| 409 | `"Order is not in pending_rating status"` | Order must be in pending_rating status |
| 409 | `"Review already exists for this order"` | Personal already submitted a review |
| 422 | Pydantic validation array | Rating out of range |

---

## GET /api/v1/reviews/order/{order_id}

Retrieve the review for a specific order. This endpoint uses the reviews router.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `order_id` | UUID | Yes | Order ID |

### Request Body

None.

### Request Example

```
GET /api/v1/reviews/order/55667788-99aa-bbcc-ddee-ff0011223344
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "aabb0011-2233-4455-6677-889900112233",
  "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "personal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "overall_rating": 5,
  "quality_rating": 5,
  "speed_rating": 4,
  "title": "专业的金融大模型微调服务",
  "comment": "智能数据科技团队对A股数据的理解非常深入，模型在研报生成任务上的表现远超我们的预期。交付速度略慢于预期，但最终质量非常高。",
  "would_repurchase": true,
  "helpful_count": 12,
  "unhelpful_count": 1,
  "created_at": "2026-03-11T11:00:00Z",
  "updated_at": "2026-03-11T11:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 404 | `"Order not found"` | Order ID does not exist |
| 404 | `"Review not found"` | No review for this order |

---

## GET /api/v1/reviews/business/{business_id}

List all reviews for a business with pagination.

**Auth:** None (public)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/reviews/business/11223344-5566-7788-99aa-bbccddeeff00?offset=0&limit=10
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "aabb0011-2233-4455-6677-889900112233",
      "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
      "personal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "overall_rating": 5,
      "quality_rating": 5,
      "speed_rating": 4,
      "title": "专业的金融大模型微调服务",
      "comment": "智能数据科技团队对A股数据的理解非常深入...",
      "would_repurchase": true,
      "helpful_count": 12,
      "unhelpful_count": 1,
      "created_at": "2026-03-11T11:00:00Z",
      "updated_at": "2026-03-11T11:00:00Z"
    },
    {
      "id": "bbcc0022-3344-5566-7788-990011223344",
      "order_id": "66778899-aabb-ccdd-eeff-223344556677",
      "personal_id": "d4e5f6a7-b8c9-0123-defa-234567890123",
      "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "overall_rating": 4,
      "quality_rating": 4,
      "speed_rating": 5,
      "title": "数据清洗服务高效可靠",
      "comment": "处理了300万条原始财报数据，清洗质量和速度都令人满意。",
      "would_repurchase": true,
      "helpful_count": 5,
      "unhelpful_count": 0,
      "created_at": "2026-03-05T09:00:00Z",
      "updated_at": "2026-03-05T09:00:00Z"
    }
  ],
  "total": 2
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"Business not found"` | Business ID does not exist |

---

## GET /api/v1/reviews/reputation/{business_id}

Retrieve the aggregated reputation score for a business. Combines multiple signal sources into a composite score.

**Auth:** None (public)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `business_id` | UUID | Yes | Business ID |

### Request Body

None.

### Request Example

```
GET /api/v1/reviews/reputation/11223344-5566-7788-99aa-bbccddeeff00
```

### Response Example

**Status: 200 OK**

```json
{
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "apparatus_accuracy_rate": 0.76,
  "apparatus_score": 8.2,
  "delta_mean": 0.83,
  "user_rating_mean": 4.5,
  "repurchase_rate": 0.85,
  "dispute_rate": 0.02,
  "total_orders": 47,
  "total_reviews": 38,
  "reputation_score": 4.7,
  "reputation_tier": "gold",
  "last_calculated_at": "2026-02-27T00:00:00Z",
  "created_at": "2026-01-15T08:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"Business not found"` | Business ID does not exist |
| 404 | `"Reputation data not found"` | No reputation data calculated yet |

---

## GET /api/v1/reviews/leaderboard

Retrieve the business reputation leaderboard, ranked by reputation score.

**Auth:** None (public)

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `limit` | int | No | Number of entries to return, default `20`, max `100` |

### Request Example

```
GET /api/v1/reviews/leaderboard?limit=5
```

### Response Example

**Status: 200 OK**

```json
[
  {
    "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
    "brand_name_zh": "智能数据科技",
    "brand_name_en": "SmartData Tech",
    "reputation_score": 92.1,
    "reputation_tier": "gold",
    "user_rating_mean": 4.5,
    "total_orders": 47
  },
  {
    "business_id": "22334455-6677-8899-aabb-ccddeeff0011",
    "brand_name_zh": "深蓝翻译",
    "brand_name_en": "DeepBlue Translation",
    "reputation_score": 87.5,
    "reputation_tier": "silver",
    "user_rating_mean": 4.3,
    "total_orders": 32
  },
  {
    "business_id": "33445566-7788-99aa-bbcc-ddeeff001122",
    "brand_name_zh": "云视觉设计",
    "brand_name_en": "CloudVision Design",
    "reputation_score": 84.3,
    "reputation_tier": "silver",
    "user_rating_mean": 4.1,
    "total_orders": 21
  }
]
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 422 | Pydantic validation array | Invalid limit value |


path: references/negotiations-api.md
content: # Negotiations API

Base URL: `/api/v1/negotiations`

Negotiation endpoints for managing sessions between personal users and businesses. Business agents use these endpoints to receive and respond to negotiation requests, send proposals, and accept/reject deals.

---

## POST /api/v1/negotiations/

Create negotiation sessions with selected businesses. **Personal only.**

**Auth:** Required (personal)

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `intention_id` | UUID | Yes | The intention to negotiate for |
| `business_ids` | list[UUID] | Yes | List of business IDs to start sessions with |

### Request Example

```json
{
  "intention_id": "f6a7b8c9-d0e1-2345-fab0-678901234567",
  "business_ids": [
    "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "c3d4e5f6-a7b8-9012-cdef-234567890abc"
  ]
}
```

### Response Example

```json
[
  {
    "id": "aabb1122-3344-5566-7788-99aabbccddee",
    "intention_id": "f6a7b8c9-d0e1-2345-fab0-678901234567",
    "personal_id": "aabbccdd-eeff-0011-2233-445566778899",
    "business_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "match_candidate_id": "11223344-aabb-ccdd-eeff-556677889900",
    "status": "active",
    "order_id": null,
    "cancelled_reason": null,
    "closed_at": null,
    "created_at": "2026-02-27T10:30:00Z",
    "updated_at": "2026-02-27T10:30:00Z",
    "business_name_zh": "智能分析科技",
    "business_name_en": "Smart Analytics Tech"
  }
]
```

### Errors

| Status | Code | Description |
|---|---|---|
| 400 | `intention_not_ready` | Intention is not in a matchable state |
| 404 | `intention_not_found` | No intention with this ID |
| 409 | `session_exists` | A session already exists with this business for this intention |

---

## GET /api/v1/negotiations/

List negotiation sessions for the current user.

**Auth:** Required

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `role` | str | No | `"personal"` or `"business"`. Default `"personal"` |
| `intention_id` | UUID \| null | No | Filter by intention |

### Response Example

```json
{
  "items": [
    {
      "id": "aabb1122-3344-5566-7788-99aabbccddee",
      "intention_id": "f6a7b8c9-d0e1-2345-fab0-678901234567",
      "personal_id": "aabbccdd-eeff-0011-2233-445566778899",
      "business_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "match_candidate_id": "11223344-aabb-ccdd-eeff-556677889900",
      "status": "active",
      "order_id": null,
      "cancelled_reason": null,
      "closed_at": null,
      "created_at": "2026-02-27T10:30:00Z",
      "updated_at": "2026-02-27T14:00:00Z",
      "business_name_zh": "智能分析科技",
      "business_name_en": "Smart Analytics Tech",
      "last_message_content": "We can deliver within 3 business days.",
      "last_message_at": "2026-02-27T14:00:00Z",
      "unread_count": 2,
      "has_pending_final_deal": false
    }
  ],
  "total": 1
}
```

---

## GET /api/v1/negotiations/{session_id}

Get details of a single negotiation session.

**Auth:** Required (personal or business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Response Example

```json
{
  "id": "aabb1122-3344-5566-7788-99aabbccddee",
  "intention_id": "f6a7b8c9-d0e1-2345-fab0-678901234567",
  "personal_id": "aabbccdd-eeff-0011-2233-445566778899",
  "business_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "match_candidate_id": "11223344-aabb-ccdd-eeff-556677889900",
  "status": "active",
  "order_id": null,
  "cancelled_reason": null,
  "closed_at": null,
  "created_at": "2026-02-27T10:30:00Z",
  "updated_at": "2026-02-27T14:00:00Z",
  "business_name_zh": "智能分析科技",
  "business_name_en": "Smart Analytics Tech"
}
```

### Errors

| Status | Code | Description |
|---|---|---|
| 401 | `not_authenticated` | Missing or invalid access token |
| 403 | `not_authorized` | User is not a party to this session |
| 404 | `session_not_found` | No session with this ID |

---

## POST /api/v1/negotiations/{session_id}/cancel

Cancel an active negotiation session.

**Auth:** Required (personal or business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Request Body

None.

### Response Example

Returns the updated session with `status: "cancelled"`.

### Errors

| Status | Code | Description |
|---|---|---|
| 403 | `not_authorized` | User is not a party to this session |
| 404 | `session_not_found` | No session with this ID |
| 409 | `session_not_active` | Session is not in active status |

---

## GET /api/v1/negotiations/{session_id}/messages

Get the message history in a negotiation session, including chat messages and contract proposals.

**Auth:** Required (personal or business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `offset` | int | No | Pagination offset. Default `0` |
| `limit` | int | No | Number of results. Default `50` |

### Response Example

```json
{
  "items": [
    {
      "id": "msg-11223344-5566-7788-99aa-bbccddeeff00",
      "session_id": "aabb1122-3344-5566-7788-99aabbccddee",
      "sender_id": "aabbccdd-eeff-0011-2233-445566778899",
      "recipient_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "message_type": "text",
      "content": "I need the analysis to cover Q1 and Q2 2026 data.",
      "attachments": null,
      "is_read": true,
      "read_at": "2026-02-27T11:05:00Z",
      "contract_data": null,
      "proposal_status": null,
      "created_at": "2026-02-27T11:00:00Z",
      "updated_at": "2026-02-27T11:05:00Z"
    },
    {
      "id": "msg-22334455-6677-8899-aabb-ccddeeff0011",
      "session_id": "aabb1122-3344-5566-7788-99aabbccddee",
      "sender_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "recipient_id": "aabbccdd-eeff-0011-2233-445566778899",
      "message_type": "proposal",
      "content": "Proposal: Financial Analysis Report",
      "attachments": null,
      "is_read": false,
      "read_at": null,
      "contract_data": {
        "terms": {"scope": "Q1-Q2 2026 financial analysis", "delivery_days": 5, "revisions": 2},
        "amount": 300.00,
        "accepted_currencies": 280.00,
        "locked_fields": ["scope"],
        "negotiable_fields": ["delivery_days", "revisions"]
      },
      "proposal_status": "open",
      "created_at": "2026-02-27T12:00:00Z",
      "updated_at": "2026-02-27T12:00:00Z"
    }
  ],
  "total": 2
}
```

---

## POST /api/v1/negotiations/{session_id}/messages

Send a text message in a negotiation session.

**Auth:** Required (personal or business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `content` | str | Yes | Message text |

### Request Example

```json
{
  "content": "Can you include a competitor comparison in the analysis?"
}
```

### Response Example

```json
{
  "id": "msg-33445566-7788-99aa-bbcc-ddeeff001122",
  "session_id": "aabb1122-3344-5566-7788-99aabbccddee",
  "sender_id": "aabbccdd-eeff-0011-2233-445566778899",
  "recipient_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "message_type": "text",
  "content": "Can you include a competitor comparison in the analysis?",
  "attachments": null,
  "is_read": false,
  "read_at": null,
  "contract_data": null,
  "proposal_status": null,
  "created_at": "2026-02-27T13:00:00Z",
  "updated_at": "2026-02-27T13:00:00Z"
}
```

### Errors

| Status | Code | Description |
|---|---|---|
| 403 | `not_authorized` | User is not a party to this session |
| 404 | `session_not_found` | No session with this ID |
| 409 | `session_not_active` | Cannot send messages in a non-active session |

---

## POST /api/v1/negotiations/{session_id}/messages/read

Mark all messages in a negotiation session as read.

**Auth:** Required (personal or business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Response Example

```json
{
  "marked_read": 3
}
```

---

## POST /api/v1/negotiations/{session_id}/propose

Send a contract proposal in a negotiation session. **Business only.**

**Auth:** Required (business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `terms` | object | Yes | Contract terms (scope, deliverables, timeline, etc.) |
| `amount` | float \| null | No | Price in USD. At least one of usd/usdc required. |
| `accepted_currencies` | float \| null | No | Price in USDC. At least one of usd/usdc required. |
| `locked_fields` | list[str] | No | Fields that cannot be changed by the other party |
| `negotiable_fields` | list[str] | No | Fields open for negotiation |
| `proposal_status` | str | No | `"open"` (default, negotiable) or `"final_deal"` (take-it-or-leave-it) |

### Request Example

```json
{
  "terms": {
    "scope": "Full Q1-Q2 financial analysis with competitor comparison",
    "delivery_days": 5,
    "revisions": 2,
    "format": "PDF + Excel dashboard"
  },
  "amount": 350.00,
  "accepted_currencies": 330.00,
  "locked_fields": ["scope"],
  "negotiable_fields": ["delivery_days", "revisions"],
  "proposal_status": "open"
}
```

### Response Example

Returns a message with `message_type: "proposal"` and `contract_data` containing the proposal details.

### Errors

| Status | Code | Description |
|---|---|---|
| 403 | `not_authorized` | User is not the business party |
| 404 | `session_not_found` | No session with this ID |
| 409 | `session_not_active` | Session is not active |
| 409 | `pending_proposal` | There is already a pending proposal in this session |

---

## POST /api/v1/negotiations/{session_id}/accept

Accept a `final_deal` proposal, creating a contract and order.

**Auth:** Required (personal or business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Request Body

None.

### Response Example

Returns the newly created `OrderResponse`:

```json
{
  "id": "11223344-5566-7788-99aa-bbccddeeff00",
  "intention_id": "f6a7b8c9-d0e1-2345-fab0-678901234567",
  "personal_id": "aabbccdd-eeff-0011-2233-445566778899",
  "business_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "session_id": "aabb1122-3344-5566-7788-99aabbccddee",
  "contract_id": "66778899-aabb-ccdd-eeff-001122334455",
  "contract_terms": {
    "scope": "Full Q1-Q2 financial analysis",
    "delivery_days": 5
  },
  "amount": 350.00,
  "accepted_currencies": 330.00,
  "paid_currency": null,
  "paid_amount": null,
  "platform_fee": 35.00,
  "status": "pending_payment",
  "created_at": "2026-02-27T15:00:00Z",
  "updated_at": "2026-02-27T15:00:00Z"
}
```

### Errors

| Status | Code | Description |
|---|---|---|
| 403 | `not_authorized` | User is not a party to this session |
| 404 | `session_not_found` | No session with this ID |
| 409 | `no_final_deal` | No final_deal proposal to accept |
| 409 | `session_not_active` | Session is not active |

---

## POST /api/v1/negotiations/{session_id}/reject

Reject a `final_deal` proposal. The session remains active and the business can send a revised proposal.

**Auth:** Required (personal or business party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Request Body

None.

### Response Example

Returns the updated session (status remains `"active"`).

### Errors

| Status | Code | Description |
|---|---|---|
| 403 | `not_authorized` | User is not a party to this session |
| 404 | `session_not_found` | No session with this ID |
| 409 | `no_final_deal` | No final_deal proposal to reject |

---

## POST /api/v1/negotiations/{session_id}/withdraw

Withdraw a previously sent proposal. The session remains active and a new proposal can be sent.

**Auth:** Required (proposal sender)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | UUID | Yes | Session ID |

### Request Body

None.

### Response Example

Returns a message with `message_type: "system"` indicating the proposal was withdrawn.

### Errors

| Status | Code | Description |
|---|---|---|
| 403 | `not_authorized` | User did not send the current proposal |
| 404 | `session_not_found` | No session with this ID |
| 409 | `no_proposal` | No active proposal to withdraw |


path: references/error-codes.md
content: # Error Codes

This document covers the standard HTTP error codes returned by the TMR Land API and their response formats.

---

## Standard Error Response Format

All error responses follow this structure:

```json
{
  "detail": "Error message describing what went wrong"
}
```

For validation errors (422), the response contains an array of field-level errors:

```json
{
  "detail": [
    {
      "loc": ["body", "field_name"],
      "msg": "Description of the validation failure",
      "type": "error_type"
    }
  ]
}
```

---

## 400 Bad Request

Returned when the request is syntactically valid but fails business logic validation.

### Common Causes

- Insufficient wallet balance for payment
- Invalid state transition (e.g. cancelling a delivered order)
- Duplicate resource creation (e.g. KYC already submitted)
- Amount exceeds limits

### Example

```json
{
  "detail": "Insufficient wallet balance"
}
```

---

## 401 Unauthorized

Returned when authentication is missing, expired, or invalid.

### Common Causes

- No `Authorization` header provided
- Bearer token is expired
- Bearer token is malformed or tampered with
- API key is invalid or revoked
- Invalid login credentials

### Example: Missing Token

```json
{
  "detail": "Not authenticated"
}
```

### Example: Invalid Credentials

```json
{
  "detail": "Incorrect email or password"
}
```

### Example: Expired Token

```json
{
  "detail": "Token has expired"
}
```

---

## 403 Forbidden

Returned when the authenticated user does not have permission to perform the requested action.

### Common Causes

- Wrong role for the endpoint (e.g. business trying to pay an order)
- Accessing a resource owned by another user
- Attempting an action reserved for the other party

### Example

```json
{
  "detail": "Only the personal user can pay for this order"
}
```

### Example: Resource Ownership

```json
{
  "detail": "Not authorized to access this intention"
}
```

---

## 404 Not Found

Returned when the requested resource does not exist.

### Common Causes

- UUID references a non-existent record
- Resource has been deleted
- Accessing a sub-resource that has not been created yet (e.g. receipt before review)

### Example

```json
{
  "detail": "Order not found"
}
```

### Example: Sub-resource

```json
{
  "detail": "Receipt not found"
}
```

---

## 409 Conflict

Returned when the request conflicts with the current state of the resource.

### Common Causes

- Invalid state transition (e.g. publishing an already published intention)
- Duplicate resource (e.g. dispute already exists for an order)
- Resource already in target state

### Example: State Transition

```json
{
  "detail": "Order is not in pending status"
}
```

### Example: Duplicate

```json
{
  "detail": "Dispute already exists for this order"
}
```

### Example: Edit Restriction

```json
{
  "detail": "Can only edit intentions in draft status"
}
```

---

## 422 Unprocessable Entity

Returned when request body fails Pydantic schema validation. This is the most common error for malformed requests.

### Common Causes

- Missing required fields
- Field type mismatch (e.g. string where integer expected)
- Value constraint violation (e.g. rating > 5, password < 8 chars)
- Invalid enum values

### Example: Missing Required Field

```json
{
  "detail": [
    {
      "loc": ["body", "email"],
      "msg": "Field required",
      "type": "missing"
    }
  ]
}
```

### Example: Invalid Value

```json
{
  "detail": [
    {
      "loc": ["body", "password"],
      "msg": "String should have at least 8 characters",
      "type": "string_too_short"
    }
  ]
}
```

### Example: Multiple Validation Errors

```json
{
  "detail": [
    {
      "loc": ["body", "email"],
      "msg": "value is not a valid email address: An email address must have an @-sign.",
      "type": "value_error"
    },
    {
      "loc": ["body", "password"],
      "msg": "String should have at least 8 characters",
      "type": "string_too_short"
    }
  ]
}
```

### Example: Invalid Enum

```json
{
  "detail": [
    {
      "loc": ["body", "category"],
      "msg": "Input should be 'data', 'model', 'agent', 'consulting', 'content' or 'other'",
      "type": "enum"
    }
  ]
}
```

### Example: Numeric Constraint

```json
{
  "detail": [
    {
      "loc": ["body", "rating"],
      "msg": "Input should be less than or equal to 5",
      "type": "less_than_equal"
    }
  ]
}
```

---

## 500 Internal Server Error

Returned when an unexpected server-side error occurs. These are not caused by client input.

### Common Causes

- Unhandled exception in application code
- Database connection failure
- External service unavailable (Elasticsearch, Milvus, LLM provider)
- Configuration error

### Example

```json
{
  "detail": "Internal server error"
}
```

---

## Error Handling Recommendations

1. **Always check the status code first.** The `detail` field provides human-readable context.
2. **For 422 errors**, iterate over the `detail` array to identify which fields failed validation and why.
3. **For 409 errors**, check the current resource state before retrying. Use GET to fetch the resource and verify its status.
4. **For 401 errors**, attempt a token refresh using `POST /api/v1/auth/refresh` before prompting re-authentication.
5. **For 500 errors**, implement exponential backoff retry logic. If the error persists, contact platform support.

---

## HTTP Status Code Summary

| Code | Name | Usage |
|---|---|---|
| 200 | OK | Successful read or update |
| 201 | Created | Successful resource creation |
| 204 | No Content | Successful deletion |
| 400 | Bad Request | Business logic validation failure |
| 401 | Unauthorized | Authentication failure |
| 403 | Forbidden | Authorization / permission failure |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | State conflict or duplicate |
| 422 | Unprocessable Entity | Request schema validation failure |
| 500 | Internal Server Error | Unexpected server error |


path: references/contracts-api.md
content: # Contracts API

Base URL: `/api/v1/contracts` and `/api/v1/contract-templates`

Personal users can view contract templates created by businesses and read their contract instances. Contract proposals are exchanged within negotiation sessions via the Negotiations API — personal users negotiate using natural language messages, while businesses send structured proposals via `POST /negotiations/{session_id}/propose`.

All endpoints require authentication.

---

## GET /api/v1/contract-templates/{template_id}

Retrieve a single contract template. Templates are created by businesses or the system and define default terms, locked fields, and negotiable fields.

**Auth:** Required

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `template_id` | UUID | Yes | Contract template ID |

### Request Body

None.

### Request Example

```
GET /api/v1/contract-templates/44556677-8899-aabb-ccdd-eeff00112233
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "44556677-8899-aabb-ccdd-eeff00112233",
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "name": "标准大模型微调服务合同",
  "default_terms": {
    "delivery_days": 14,
    "revision_rounds": 2,
    "data_retention_days": 90,
    "ip_ownership": "personal",
    "confidentiality": true,
    "sla_uptime": "99.5%",
    "payment_schedule": "escrow_full"
  },
  "locked_fields": ["confidentiality", "payment_schedule"],
  "negotiable_fields": ["delivery_days", "revision_rounds", "data_retention_days", "ip_ownership"],
  "status": "published",
  "is_system_template": false,
  "created_at": "2026-01-20T10:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 404 | `"Contract template not found"` | Template ID does not exist |

---

## GET /api/v1/contracts/

List contract instances for the current user.

**Auth:** Required

### Query Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `role` | str | No | Filter by role perspective, default `"personal"` |
| `offset` | int | No | Pagination offset, default `0` |
| `limit` | int | No | Items per page, default `20`, max `100` |

### Request Example

```
GET /api/v1/contracts/?role=personal&offset=0&limit=10
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "items": [
    {
      "id": "66778899-aabb-ccdd-eeff-001122334455",
      "template_id": "44556677-8899-aabb-ccdd-eeff00112233",
      "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
      "personal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "terms": {
        "delivery_days": 14,
        "revision_rounds": 2,
        "data_retention_days": 90,
        "ip_ownership": "personal",
        "confidentiality": true,
        "sla_uptime": "99.5%",
        "payment_schedule": "escrow_full"
      },
      "amount": 200.00,
      "accepted_currencies": null,
      "confirmed_at": "2026-02-27T16:00:00Z",
      "created_at": "2026-02-27T14:00:00Z",
      "updated_at": "2026-02-27T16:00:00Z"
    }
  ],
  "total": 1
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |

---

## GET /api/v1/contracts/{contract_id}

Retrieve a single contract instance. Both parties can access.

**Auth:** Required (party)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `contract_id` | UUID | Yes | Contract instance ID |

### Request Body

None.

### Request Example

```
GET /api/v1/contracts/66778899-aabb-ccdd-eeff-001122334455
[REDACTED_SECRET]
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "66778899-aabb-ccdd-eeff-001122334455",
  "template_id": "44556677-8899-aabb-ccdd-eeff00112233",
  "business_id": "11223344-5566-7788-99aa-bbccddeeff00",
  "personal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "terms": {
    "delivery_days": 21,
    "revision_rounds": 3,
    "data_retention_days": 90,
    "ip_ownership": "personal",
    "confidentiality": true,
    "sla_uptime": "99.5%",
    "payment_schedule": "escrow_full"
  },
  "amount": 200.00,
  "accepted_currencies": null,
  "confirmed_at": "2026-02-27T16:00:00Z",
  "created_at": "2026-02-27T14:00:00Z",
  "updated_at": "2026-02-27T15:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid Bearer token |
| 403 | `"Not authorized to access this contract"` | User is not personal or business |
| 404 | `"Contract not found"` | Contract ID does not exist |


path: references/auth-api.md
content: # Authentication API

Base URL: `/api/v1/auth`

All endpoints in this section are **public** (no authentication required).

---

## POST /api/v1/auth/register

Register a new user account.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | EmailStr | Yes | User email address |
| `password` | str | Yes | Password, minimum 8 characters |
| `display_name` | str \| None | No | Display name, max 100 characters |
| `locale_preference` | str | No | Preferred locale, default `"zh"` |

### Request Example

```json
{
  "email": "[REDACTED_SECRET]",
  "password": "securePass123",
  "display_name": "张明",
  "locale_preference": "zh"
}
```

### Response Example

**Status: 201 Created**

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "[REDACTED_SECRET]",
  "display_name": "张明",
  "avatar_url": null,
  "role": "user",
  "is_active": true,
  "is_email_verified": false,
  "kyc_status": "unverified",
  "locale_preference": "zh",
  "theme_preference": "system",
  "created_at": "2026-02-27T10:30:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Email already registered"` | Email already exists in the system |
| 422 | Pydantic validation array | Invalid email format or password too short |

---

## POST /api/v1/auth/login

Authenticate with email and password.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | EmailStr | Yes | Registered email address |
| `password` | str | Yes | Account password |

### Request Example

```json
{
  "email": "[REDACTED_SECRET]",
  "password": "securePass123"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJleHAiOjE3NDA3MjAwMDB9.abc123",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJ0eXBlIjoicmVmcmVzaCJ9.def456",
  "token_type": "bearer"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Incorrect email or password"` | Invalid credentials |
| 401 | `"Account is deactivated"` | User account is inactive |
| 422 | Pydantic validation array | Invalid email format |

---

## POST /api/v1/auth/login/code

Authenticate with email and a 6-digit verification code.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | EmailStr | Yes | Registered email address |
| `code` | str | Yes | 6-character verification code |

### Request Example

```json
{
  "email": "[REDACTED_SECRET]",
  "code": "482917"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJleHAiOjE3NDA3MjAwMDB9.abc123",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJ0eXBlIjoicmVmcmVzaCJ9.def456",
  "token_type": "bearer"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Invalid or expired verification code"` | Code is wrong or expired |
| 404 | `"User not found"` | No account with that email |
| 422 | Pydantic validation array | Invalid email or code format |

---

## POST /api/v1/auth/send-code

Send a verification code to the user's email for passwordless login.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | EmailStr | Yes | Registered email address |

### Request Example

```json
{
  "email": "[REDACTED_SECRET]"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "message": "Verification code sent",
  "code_preview": "482917"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"User not found"` | No account with that email |
| 422 | Pydantic validation array | Invalid email format |

---

## POST /api/v1/auth/refresh

Exchange a refresh token for a new access/refresh token pair.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `refresh_token` | str | Yes | Valid refresh token from a previous login |

### Request Example

```json
{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJ0eXBlIjoicmVmcmVzaCJ9.def456"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJleHAiOjE3NDA3MjUwMDB9.ghi789",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJ0eXBlIjoicmVmcmVzaDIifQ.jkl012",
  "token_type": "bearer"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 401 | `"Invalid or expired refresh token"` | Token is malformed, expired, or revoked |

---

## POST /api/v1/auth/forgot-password

Request a password reset code sent to the user's email.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | EmailStr | Yes | Registered email address |

### Request Example

```json
{
  "email": "[REDACTED_SECRET]"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "message": "Password reset code sent",
  "code_preview": "739281"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 404 | `"User not found"` | No account with that email |
| 422 | Pydantic validation array | Invalid email format |

---

## POST /api/v1/auth/verify-reset-code

Verify a password reset code and receive a reset token. Call this between `forgot-password` and `reset-password`.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | EmailStr | Yes | Registered email address |
| `code` | str | Yes | 6-character reset code from email |

### Request Example

```json
{
  "email": "[REDACTED_SECRET]",
  "code": "739281"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "reset_token": "eyJhbGciOiJIUzI1NiIs..."
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Invalid or expired reset code"` | Code is wrong or expired |
| 404 | `"User not found"` | No account with that email |

---

## POST /api/v1/auth/reset-password

Reset the user's password using a reset token/code.

**Auth:** None

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `token` | str | Yes | Password reset token received via email |
| `new_password` | str | Yes | New password, minimum 8 characters |

### Request Example

```json
{
  "token": "739281",
  "new_password": "newSecurePass456"
}
```

### Response Example

**Status: 200 OK**

```json
{
  "message": "Password has been reset successfully"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Invalid or expired reset token"` | Token is wrong or expired |
| 422 | Pydantic validation array | Password too short |


path: references/disputes-api.md
content: # Disputes API

Disputes are resolved by the **Agent Congress** — a panel of 9 AI jurors that independently evaluate evidence and vote. There is no manual negotiation, messaging, or escalation flow. When a dispute is created (via `POST /api/v1/orders/{order_id}/dispute`), it enters `voting` status and the Congress service processes it asynchronously.

All endpoints require authentication.

---

## POST /api/v1/orders/{order_id}/dispute

Create a dispute on an order. Triggers Agent Congress voting.

**Auth:** Required (personal)

### Path Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `order_id` | UUID | Yes | Order ID |

### Request Body

| Field | Type | Required | Description |
|---|---|---|---|
| `reason` | str | Yes | Dispute reason, minimum 10 characters |
| `evidence` | list[str] | No | URLs to evidence files, default `[]` |
| `refund_type` | str | No | `"full"` (default) or `"partial"` |
| `requested_refund_amount` | float \| null | No | Required when `refund_type` is `"partial"`. Must be > 0 |

### Request Example

```json
{
  "reason": "The delivered model's accuracy is 45%, far below the 70% baseline specified in the contract.",
  "evidence": [
    "https://cdn.tmrland.com/evidence/eval-report-55667788.pdf"
  ],
  "refund_type": "partial",
  "requested_refund_amount": 5000.00
}
```

### Response Example

**Status: 200 OK**

```json
{
  "id": "77889900-aabb-ccdd-eeff-112233445566",
  "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "initiator_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "reason": "The delivered model's accuracy is 45%, far below the 70% baseline specified in the contract.",
  "evidence": ["https://cdn.tmrland.com/evidence/eval-report-55667788.pdf"],
  "contract_snapshot": {"delivery_days": 14, "revision_rounds": 2},
  "refund_type": "partial",
  "requested_refund_amount": 5000.00,
  "status": "voting",
  "resolution": null,
  "resolution_notes": null,
  "refund_amount": null,
  "votes_buyer": 0,
  "votes_seller": 0,
  "avg_confidence": null,
  "resolved_at": null,
  "created_at": "2026-03-12T09:00:00Z",
  "updated_at": "2026-03-12T09:00:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 400 | `"Order is not in a disputable status"` | Order not in delivering/pending_review/revision_requested |
| 404 | `"Order not found"` | Order ID does not exist |
| 409 | `"Dispute already exists"` | Order already has a dispute |

---

## GET /api/v1/orders/{order_id}/dispute

Retrieve the dispute associated with an order.

**Auth:** Required (party or admin)

### Response Example

**Status: 200 OK**

Same shape as the dispute creation response. After Congress voting completes, `status` becomes `"resolved"`, and `resolution`, `resolution_notes`, `refund_amount`, `votes_buyer`, `votes_seller`, and `avg_confidence` are populated.

```json
{
  "id": "77889900-aabb-ccdd-eeff-112233445566",
  "order_id": "55667788-99aa-bbcc-ddee-ff0011223344",
  "initiator_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "reason": "The delivered model's accuracy is 45%, far below the 70% baseline specified in the contract.",
  "evidence": ["https://cdn.tmrland.com/evidence/eval-report-55667788.pdf"],
  "contract_snapshot": {"delivery_days": 14, "revision_rounds": 2},
  "refund_type": "partial",
  "requested_refund_amount": 5000.00,
  "status": "resolved",
  "resolution": "partial_refund",
  "resolution_notes": "Majority ruled partial refund due to quality shortfall.",
  "refund_amount": 4000.00,
  "votes_buyer": 6,
  "votes_seller": 3,
  "avg_confidence": 0.82,
  "resolved_at": "2026-03-12T09:05:00Z",
  "created_at": "2026-03-12T09:00:00Z",
  "updated_at": "2026-03-12T09:05:00Z"
}
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 403 | `"Not authorized"` | User is not a party or admin |
| 404 | `"No dispute found"` | No dispute exists for this order |

---

## GET /api/v1/orders/{order_id}/dispute/votes

Get individual Agent Congress juror votes for the order's dispute.

**Auth:** Required (party or admin)

### Response Example

**Status: 200 OK**

```json
[
  {
    "id": "aa112233-4455-6677-8899-aabbccddeeff",
    "dispute_id": "77889900-aabb-ccdd-eeff-112233445566",
    "juror_index": 1,
    "juror_name": "Contract Analyst",
    "juror_role": "Evaluates whether delivery meets contractual obligations",
    "vote": "buyer",
    "reasoning": "The contract specifies 70% accuracy baseline. The delivered model achieves only 45%, a clear breach of terms.",
    "confidence": 0.95,
    "created_at": "2026-03-12T09:03:00Z"
  },
  {
    "id": "bb223344-5566-7788-99aa-bbccddeeff00",
    "dispute_id": "77889900-aabb-ccdd-eeff-112233445566",
    "juror_index": 2,
    "juror_name": "Technical Reviewer",
    "juror_role": "Assesses technical quality of deliverables",
    "vote": "buyer",
    "reasoning": "Evaluation report confirms accuracy shortfall. The methodology appears sound.",
    "confidence": 0.88,
    "created_at": "2026-03-12T09:03:00Z"
  }
]
```

### Errors

| Status | Detail | Condition |
|---|---|---|
| 403 | `"Not authorized"` | User is not a party or admin |
| 404 | `"No dispute found"` | No dispute exists for this order |

---

## GET /api/v1/disputes/{dispute_id}

Retrieve full dispute details (admin or party). Returns the dispute with Congress vote details.

**Auth:** Required (party or admin)

### Response Example

**Status: 200 OK**

Same fields as `GET /orders/{order_id}/dispute` plus a `votes` array with Congress juror details.

### Errors

| Status | Detail | Condition |
|---|---|---|
| 403 | `"Not a party to this dispute"` | User is not a party or admin |
| 404 | `"Dispute not found"` | Dispute ID does not exist |

---

## Dispute Status Flow

```
voting → resolved
```

- `voting`: Congress AI jurors are evaluating the dispute
- `resolved`: Verdict delivered. Resolution is one of: `full_refund`, `partial_refund`, `seller_wins`

If Congress voting times out (>1 hour), the dispute auto-resolves with `full_refund` as a safety net.

## Order Status After Dispute

- `disputed`: Order moves to this status when a dispute is created
- `refunded`: Order moves to this status when the dispute resolves with a refund (full or partial)
- Order returns to normal flow if `seller_wins`


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)