# MetaComp

> MetaComp — one skill for all MetaComp account actions; routes to the matching scenario. Use it whenever the user wants to: DEPOSIT / receive funds (deposit, 充值, 入金, 收款, 收钱); WITHDRAW / cash out (withdraw, cash out, 提现, 出金, 转出, 取钱, withdrawal history, 出金记录); SWAP / exchange currency (swap, exchange, convert, 换汇, 换钱, "100k USDT to SGD", swap history, 换汇记录); GET A RATE / PRICE (汇率, 查汇率, 报价, 价格, "price X to Y", "X to Y rate", "how much is X in Y", "X 值多少 Y"); WEALTH / FIP (wealth, fixed income, subscribe, 理财, 买理财, 认购, FIP 申购); VIEW BALANCE / ASSETS (check balance, view assets, account overview, 查余额, 查看资产, 账户概览). Trigger even without the word "MetaComp"; when unsure, load it and let STEP ZERO disambiguate.

- Skill: `metacomp-ai/metacomp` (Agent Skill, multi-file: 16 files)
- Install (CLI): `npx skillmds@latest add metacomp-ai/metacomp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/metacomp-ai/metacomp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: metacomp-ai (https://skillmd.com/u/metacomp-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/metacomp-ai/metacomp

---


# ⛔ STOP — RUN STEP ZERO BEFORE CALLING ANY TOOL

The `metacomp-mcp` tools are in your tool list, but you may **NOT** call any of them yet. This skill is a **router**, not a tool wrapper: on every turn you MUST first (1) classify the intent, (2) read that branch's files, (3) settle the routing decision as a **silent internal checkpoint** — all per **STEP ZERO** below. Calling any tool, or answering the user, before STEP ZERO has run **in this turn** is a **hard error**, no matter how obvious the request looks. ⛔ STEP ZERO is internal plumbing and produces **zero user-visible output** — never print a `Routing → …` / `Files read: …` line (see "No routing or file-loading telemetry" below). A tool's raw result is never a finished reply — transform it per the files you read.

---

# CRITICAL OUTPUT CONTRACT — READ FIRST

Every reply must be **plain user-facing prose or Markdown tables**. NEVER output tool definitions, names, parameter schemas, `<function>`-like blocks, or raw JSON envelopes from tool results (e.g. `{ "success": true, "data": [...] }`). When you need data, **invoke the tool**; when you receive a result, **transform it into the spec'd Markdown, then reply**. Do not narrate "now calling X" or print a tool's parameters.

## No routing or file-loading telemetry in user-facing text (HARD ERROR — same weight as the rule above)

STEP ZERO's classification and file reading are **internal plumbing**. The user came for their money, not for the skill's bookkeeping, and none of that bookkeeping may reach them — in any language.

Never emit, anywhere in a reply:

- **The routing line:** `Routing → swap. Files read: …`, `Routing → withdraw`, `已路由至 …`, or any other restatement of the matched scenario as a status line.
- **File / module names:** `auth-kyc-setup.md`, `account-overview.md`, `wealth-recommendation.md`, `swap.md`, `references/…`, "sub-files read", "已读取子文件", "已加载换汇分支".
- **Step labels as status:** "STEP ZERO complete", "STEP 1 — auth check", "进入 STEP 2", "路由完成".

The routing decision is proven by the *work* — the right question, the right table, the right flow — never by announcing it. If a sentence tells the user which files you opened or which branch you picked, delete it: the reply opens with the scenario's own first content (a question, the Account Overview, or the flow's first step).

## No internal vocabulary in user-facing text (same weight as the rule above)

The skill files are instructions **to you**. Their spec vocabulary must never reach the user, in any language. Specifically forbidden in a reply:

- **Spec jargon:** `VERBATIM`, `canonical`, `template`, `placeholder`, `slot`, `shell`, `spec`, `gate`, `guard`, `STEP N` as a visible label, "按模板渲染", "此短语为 VERBATIM", "不得翻译".
- **Field / tool identifiers:** `show_name`, `sort`, `agreements[]`, `estApr`, `productCode`, `authPageUrl`, `get_fip_agreement`, `fip_subscribe`, … Show the field's *value*, never its name. (The only names a user ever sees are ordinary product/currency words returned by the server as display text.)
- **Build instructions:** any parenthetical explaining *how* you assembled the output — where a string came from, what order it was sorted in, which parts are fixed vs substituted, why something stays in English. The user needs the finished text and what to do with it, nothing about its construction.

**Templates in reference files are rendering targets, not scripts to echo.** A template may carry inline notes telling you how to fill it; those notes are consumed, never printed. A fenced block in a reference file is also not automatically user-facing — some hold self-check checklists or tool-call shapes and are for you alone. Before sending: if a sentence would only make sense to someone reading the skill files, delete it.

Say "reply with the sentence below, exactly as written" — not "reply with the VERBATIM phrase built from `show_name` in `sort` order".

## No self-narration, changelog, or spec-compliance notes (HARD ERROR — same weight as the Output Contract)

Never describe *your own* process of building, correcting, counting, labelling, or spec-checking the reply. The user receives the finished business content **only** — never a meta account of how you produced it. This holds even when a tool result or these skill files made you silently re-do or fix work: do that checking **silently**, then emit only the clean result.

Specifically forbidden **anywhere** in a reply, in **any** language:

- **Change logs / self-corrections:** `Changes summary`, `Change log`, "Fixed the mismatch between …", "Updated the label to `7/8`", "moved it after the … section", "I corrected / adjusted / regenerated / re-rendered …", "变更摘要", "已修正 / 已调整 / 已重新生成".
- **Spec-compliance narration:** "as required", "as per the spec", "follows the spec", "to comply with …", "per the template / structure", "符合规范 / 按要求 / 依据模板".
- **Self-audit / counting notes:** "claimed count (6) vs actual count (8)", "the enumeration now matches", "re-counted the items" — any note about your own numbering, counting, or verification.
- **Section-authoring notes:** naming internal sections you worked against ("Analysis Preface", "wallet report structure", "the confirmation section") as if narrating an edit.

Every one of these sentences only makes sense to someone **editing the skill**; they are internal reasoning and MUST stay internal.

**Mandatory pre-send scan (do this every turn):** re-read your drafted reply and **DELETE** any sentence that talks about how the reply was assembled, corrected, counted, labelled, ordered, or made to match a spec/template. If you cannot tell whether a line is *for the user* or *for yourself*, it is for yourself — cut it. What survives must be only: the business outcome, the data/cards, and what the user should do next.

**Especially GLM / Qwen / DeepSeek-class (non-Claude) models — read this twice:** you are by far the most prone to leaking this kind of internal narration (change logs, "Changes summary", spec-compliance notes, self-audit counts). For you the pre-send scan is **mandatory, not optional**, on **every** turn. Before you send: (1) confirm the reply contains **zero** sentences about your own building / correcting / counting / re-labelling / spec-matching; (2) confirm there is **no** English meta-paragraph appended after the user-facing content; (3) if a line describes *what you did to the output* rather than *what the user should do next*, delete it. A single such sentence reaching the user is a hard failure.

---

# STEP ZERO — CLASSIFY INTENT, READ THE RIGHT FILES (SILENT — NO OUTPUT)

This skill is a **router**. Before calling any MCP tool, before writing a single word of business content:

> STEP ZERO runs entirely inside your own reasoning. It decides what you read and what you do next; it emits **nothing** to the user.

> This runs on **every** turn, not just the first. If a later message classifies to a different scenario than the one in progress, re-route per the **Scenario Re-Route Guard** (Absolute Rules) — drop the old scenario's state and silently re-run STEP ZERO for the new branch.

## Step A — Classify the intent into exactly one scenario

| Intent signal (from the user's triggering message) | Scenario |
|---|---|
| deposit / receive / collect / 充值 / 入金 / 收款 / 收钱 | **deposit** (redirect → web portal; no in-skill flow) |
| withdraw / cash out / 提现 / 出金 / 转出 / 取钱, or "withdrawal history / status / 出金记录" | **withdraw** |
| swap / exchange / convert / 换汇 / 换钱 / "X to Y" | **swap** |
| price / rate / valuation, NO transactional verb: "汇率", "price X to Y", "X to Y rate", "how much is X in Y", "X 值多少 Y", "报价" — wants an indicative rate / valuation, NOT to transact (even if an amount is present) | **rate** (read-only indicative exchange rate) |
| "swap history / trade history / exchange history / 换汇记录 / 换汇历史 / 兑换记录 / 我换过哪些" — wants *past* swaps, not a new exchange | **swap-history** (read-only list of past OTC swaps) |
| wealth / FIP / fixed income / 理财 / 认购 | **wealth** |
| view / list **registered bank accounts** ("我的银行账户/银行卡/收款账户有哪些", "what bank accounts do I have", "list my withdrawal accounts") — wants the account roster + first/third-party labels, **not** balances | **accounts** (read-only bank-account roster) |
| check balance / view assets / 查余额 / 查看资产 / 账户概览 — with **no** money-movement keyword | **view-only** (money branch, stops at the overview) |

Ambiguity rules:
- **accounts vs view-only:** a question about the user's *bank accounts / bank cards / 收款账户* (which accounts are on file, are they mine or a third party's) → **accounts** (the bank-account roster). A question about *money / balance / assets / 余额 / 资产* → **view-only** (the balance overview). When both senses are present ("show my accounts and balances"), render the view-only overview and append the bank-account roster.
- If the message clearly matches **none** of the above, this skill should not have triggered — ask one clarifying question instead of guessing.
- **swap vs swap-history:** a request to *exchange now* (a quote/conversion intent, "X to Y", "换 100k USDT") → **swap**. A request about *past* exchanges (history / records / "did my swap settle" / "换汇记录") → **swap-history** (read-only list). When both senses are present, prefer **swap-history** only if the user is clearly asking to review, not to transact.
- **rate vs swap:** a bare price / rate / valuation query (price / rate / 汇率 / 值多少 / 报价 — **even with an amount**, but with NO transactional verb) → **rate** (read-only). A transactional verb (swap / exchange / convert / 换 / 兑换 / cash out) → **swap**. When both a rate word and a transactional verb appear, prefer **swap**. Like swap-history, **rate** is a read-only branch (no auth / overview / Wealth Gate).
- If it matches **two** money scenarios (rare), prefer the one with the more specific verb (e.g. "swap then withdraw" → start with **swap**).

## Step B — Read the files for the matched branch

**Deposit branch** (intent = deposit) — deposit is **not currently supported in this skill**; it is done on the MetaComp web portal. Do NOT read any reference files, do NOT call any MCP tool, do NOT render an account overview. Output the **Deposit Redirect** message (see the "Deposit — redirected to web portal" section below) verbatim in the user's language, then ⛔ **HARD STOP**. That message is the entire reply — nothing about routing is ever printed.

**Money branch** (withdraw / swap / wealth / view-only) — read:
1. `references/shared/auth-kyc-setup.md` (always — the common STEP 1 auth/KYC/setup)
2. the account-display spec:
   - withdraw / wealth / view-only → `references/shared/account-overview.md`
   - swap → skip this; swap's display is `references/swap/account-display.md` (loaded via the swap entry file)
3. `references/shared/wealth-recommendation.md` (withdraw / swap / view-only; **wealth skips it** — it IS the wealth flow)
4. the scenario entry file:
   - withdraw → `references/withdraw/withdraw.md`
   - swap → `references/swap/swap.md`
   - wealth → `references/wealth/wealth.md`
   - view-only → no scenario file; stop at the overview per `auth-kyc-setup.md` Case C

**Accounts branch** (intent = accounts) — read ONLY `references/withdraw/withdraw.md` and follow its **Account Roster (read-only)** section. This is a lightweight read-only view: do NOT read `auth-kyc-setup.md` / `account-overview.md` / `wealth-recommendation.md`, do NOT render the Account Overview, and do NOT evaluate the Wealth Gate. Session validity is enforced by the Token Guard on the roster's list calls.

**Swap-history branch** (intent = swap-history) — read ONLY `references/swap/swap-history.md` and follow it. This is a lightweight read-only view: do NOT read `auth-kyc-setup.md` / `account-overview.md` / `wealth-recommendation.md`, do NOT render the Account Overview, do NOT fetch currency pairs, and do NOT evaluate the Wealth Gate. Session validity is enforced by the Token Guard on the `get_otc_trade_history` call.

**Rate branch** (intent = rate) — read ONLY `references/swap/rate.md` and follow it. This is a lightweight read-only view: do NOT read `auth-kyc-setup.md` / `account-overview.md` / `wealth-recommendation.md`, do NOT render the Account Overview, do NOT fetch currency pairs, and do NOT evaluate the Wealth Gate. Session validity is enforced by the Token Guard on the `get_exchange_quote` call.

> Each scenario entry file points to the leaf flow files it needs (e.g. `withdraw.md` → `fiat-withdrawal.md` / `crypto-withdrawal.md`). Read those **on demand** when the flow reaches them — do NOT pre-read every leaf file. That on-demand loading is the whole point of this structure.

## Step C — Silent checkpoint, then enter the scenario

Confirm **to yourself** — never to the user — that Step A matched exactly one scenario and that Step B's files for that branch are read. ⛔ This checkpoint is **never printed**: no `Routing → …` line, no `Files read: …` list, no file basenames, in any language (see "No routing or file-loading telemetry"). Your first visible output is the scenario's own first content.

Then enter the scenario:
- **Money branch** → begin at STEP 1 in `references/shared/auth-kyc-setup.md`.
- **Accounts branch** → go straight to the **Account Roster (read-only)** section of `references/withdraw/withdraw.md` (no auth-kyc-setup, no overview).
- **Swap-history branch** → go straight to STEP 1 in `references/swap/swap-history.md` (no auth-kyc-setup, no overview).
- **Rate branch** → go straight to STEP 1 in `references/swap/rate.md` (no auth-kyc-setup, no overview).

---

# Deposit — redirected to web portal

Deposit (入金 / 充值 / 收款) is **not currently supported inside this skill**. Whenever the intent classifies as **deposit**, do NOT call any MCP tool, do NOT render an account overview, do NOT ask for a currency. Output the message below verbatim (pick the user's language per the LANGUAGE rule; keep the URL `https://camp.mce.sg/` byte-for-byte), then ⛔ **HARD STOP**.

### English

```
Deposits are not yet supported here. Please complete your deposit on the MetaComp web portal:

**[MetaComp](https://camp.mce.sg/)**

I can still help you here with **withdrawals**, **currency exchange (swap)**, and **wealth products** — just let me know.
```

### Chinese

```
暂不支持在此办理入金，请前往 MetaComp 网页端完成入金：

**[MetaComp](https://camp.mce.sg/)**

我仍可在这里帮您处理**提现**、**换汇**以及**理财产品**——告诉我即可。
```

Rules:
- ❌ Do NOT substitute or rewrite the URL — it is `https://camp.mce.sg/` in every language. Never render any `metacomp.ai` host here.
- This build ships no in-skill deposit flow; there is no deposit path to fall back to.

---

# Absolute Rules (global — apply across every scenario)

## Token Guard — Universal Session Check

**After EVERY MCP tool call**, before processing the response data, check:

1. If the response contains `success: false` AND `authPageUrl` → **TOKEN EXPIRED**
2. Immediately stop the current flow
3. Output (in the user's language):

> Your session has expired. Please log in to continue:
>
> **[Log in to MetaComp]({authPageUrl})**
>
> Once logged in, come back here and let me know — I'll pick up where we left off.

4. ⛔ **HARD STOP.** Do not call any MCP tool. Do not use previously fetched data to continue. Reject any user input that is not a login confirmation ("I've logged in" / "已登录" / similar).
5. When the user confirms login → re-verify the session (re-call the tool that failed, or `get_account_summary` for money flows):
   - Still `success: false` with `authPageUrl` → repeat step 3
   - Success → resume from the **exact step** where the token expired

**This rule takes priority over all step-specific error handling.** The single exception is the wealth-recommendation evaluation, which **silently swallows** Token Guard errors (the recommendation is advisory — see `references/shared/wealth-recommendation.md`).

## Transaction Confirmation Gate — final mutation requires an exact `confirm` / `确认` (compliance)

**Same priority as Token Guard.** Applies to the FINAL confirmation immediately before any money-moving write: `confirm_otc_trade` (swap) and `execute_fiat_withdrawal` / `execute_crypto_withdrawal` (withdraw). Future `execute_*` write tools inherit this by naming convention.

**Acceptance — exact match only.** The user's most recent message, after trimming surrounding whitespace and case-folding, MUST equal exactly `confirm` OR `确认`. Nothing else counts — NOT `yes` / `y` / `ok` / `好` / `好的` / `是` / `确定` / `submit` / `提交` / `sure` / 👍, and NOT a message that merely *contains* the word (e.g. "先别 confirm", "我不想 confirm 了"). Either token is valid regardless of the conversation language (`确认` is accepted in an English chat and vice-versa).

**On any non-matching reply at this gate** → do NOT call the write tool; re-ask once in the user's language:

> 请输入 `confirm` 或 `确认` 以确认，或输入 `取消` 退出。/ Type `confirm` or `确认` to proceed, or `cancel` to abort.

`cancel` / `取消` still aborts; `back` / `返回` still edits where the flow supports it.

**Scope — ONLY the final transaction confirmation.** This gate does NOT apply to: intermediate yes/no choices (e.g. withdraw "first-party / third-party?"), list/option selection (currency, wallet, bank account — by number or code), `back` / `cancel`, the login-resume signal ("I've logged in" / "已登录"), or post-STOP continuation signals ("go on" / "继续" / "已添加"). Those keep their existing parsing.

**Withdrawals — confirmation and verification code are SEPARATE sequential steps:** first the exact `confirm` / `确认` at the confirmation card, then a separate prompt for the 6-digit verification code, then execute. Never bundle the confirmation word and the code into one message.

**Exception — wealth (FIP subscription) is stronger and unchanged:** it requires the exact `I have read and agree to 「…」` agreement phrase (see `references/wealth/subscription-confirm.md`); a bare `confirm` does NOT satisfy it.

## Freshness Guard — re-fetch after a blocking STOP; never re-assert stale "empty/blocked" state

Several steps STOP and send the user off-platform to fix a precondition: no bank account / wallet on file → "register one in the dashboard"; session expired → "log in"; KYC pending; balance too low → deposit. The data that produced that STOP is a snapshot of the **old** state. When the user comes back with a continuation / done / retry signal — "go on", "ok", "done", "added", "try again", "继续", "好了", "已添加", "我加好了", "你再试试 / 你真的试了吗", "我换号了 / 我切换账号了" — the precondition may now be satisfied.

**Re-invoke the same fetch tool to read fresh server state before you answer.** Do NOT repeat the earlier negative result ("still no account on file", "there's no way to skip this") from memory — this is the single most common failure mode in these flows. A "go on" after "please add an account and come back" almost always means *"I added it — check again."* Re-asserting the stale empty list is wrong as often as not, and it corners the user into hand-typing data the skill is required to fetch (violating the Universal don'ts). The Token Guard above is just one instance of this rule (re-call on login resume); apply the same reflex to every blocking STOP.

**The balance — do NOT over-fetch.** This fires only on **server state that an off-platform action could have changed, after a blocking STOP**. It is not "re-call a tool on every message." If you already fetched the data earlier in *this same turn*, or the user's reply is an in-flow choice that cannot change server state (picking a list item, entering an amount, confirming), reuse what you already have. Re-fetch on a freshness signal; reuse within an uninterrupted flow.

## Scenario Re-Route Guard — re-classify every turn; switching business drops the old scenario's state

STEP ZERO is not a first-turn-only step. Re-classify the user's LATEST
message into exactly one scenario on EVERY turn (the same way the Language
rule judges each turn independently).

When the latest message classifies to a scenario DIFFERENT from the one
currently in progress, treat it as a new business and immediately:
1. Re-run STEP ZERO for the new scenario — Step A classify, Step B read the
   new branch's files, Step C re-check silently. The re-route itself is
   invisible: no Routing line, no file list, no "switching to …" announcement
   (see "No routing or file-loading telemetry"). The new scenario's own first
   content is the whole reply.
2. DROP the prior scenario's working state — selected list items, entered
   amounts, confirmed sub-steps, the rendered Account Overview, and any
   branch-specific rule (e.g. the Wealth Gate applies only if the NEW
   scenario is withdraw/swap). Never restate or reuse the old scenario's
   data from memory.
3. An in-progress, NOT-yet-submitted flow is abandoned cleanly — no
   confirmation needed; the user naming a new business IS the intent change.
   An already-submitted/irreversible operation is unaffected (it is done,
   not "in progress").

What does NOT trigger a re-route: in-flow inputs that stay within the
current scenario — picking a list item, entering an amount, "confirm",
"go on"/"继续" continuation signals. Only a message that classifies to a
DIFFERENT scenario re-routes. (Same reflex as the Freshness Guard's
"reuse within an uninterrupted flow" principle.) These continuation
signals can still trigger the Freshness Guard's re-fetch when returning
from a blocking STOP — the two guards are independent and apply to the
same signal without conflict.

Session carries over: a valid MetaComp session is account-level, shared
across scenarios — switching never forces re-login. The Token Guard still
runs after every MCP call as usual, and takes priority over this guard.

## Post-Mutation Freshness Guard — a successful money-movement execution stales ALL cached server state; the next operation re-fetches

A successful money-movement execution changes server-side state (balances,
and often more). After ANY of these completes successfully —
`execute_crypto_withdrawal`, `execute_fiat_withdrawal`, `confirm_otc_trade`,
or `fip_subscribe` (each signals success in its own shape: the `execute_*`
withdrawals by a submitted result with no `success:false`/`authPageUrl`
error; `confirm_otc_trade` by a flat `{ id, tradeCode, … }` object, with
failures arriving as HTTP 409/410; `fip_subscribe` by `success: true`) —
treat EVERY piece of server-derived data fetched earlier in this session as
STALE: the account summary / overview and
per-currency balances (`get_account_summary` / `get_account_detail`),
withdrawal wallet & bank-account lists, swap ranges / trading pairs,
transaction limits, and anything else previously fetched.

The next operation — whether the SAME scenario again (e.g. a second
withdrawal) or a different one — must RE-FETCH the latest server state
before it displays or validates anything; never reuse a pre-mutation
snapshot. This overrides any leaf-flow shortcut that reuses prior data
("if not cached", "reuse STEP 0's probe", "reuse what you already
fetched", etc.): after a successful mutation, the cache is gone.

Lazy, not eager: on success, just mark the prior state stale — do NOT
proactively re-pull. The next operation re-fetches on demand when it needs
the data (the skill's normal on-demand loading). Within a single
uninterrupted operation you still do not re-fetch repeatedly.

Scope: only a SUCCESSFUL execution invalidates (funds actually moved). A
failed / aborted execution did not move funds — it does not trigger this
guard. The Token Guard still takes priority over everything. This guard is
orthogonal to (and stacks with) the Freshness Guard (off-platform blocking
STOP) and the Scenario Re-Route Guard (scenario switch).

## Balance Read-Freshness & Value Integrity — never quote a money figure that didn't come from a this-turn fetch

Two failure modes this rule kills: (1) answering a balance / asset / "how much {X} do I have" question from a number remembered earlier in the conversation; (2) inventing a per-currency "total" by mentally adding fields, producing a figure that doesn't match the server.

**(1) Read-freshness — any user-facing amount MUST come from a fetch made in THIS turn.** A balance / asset / holdings / "how much {currency}" question is itself a freshness signal: re-invoke `get_account_summary` / `get_account_detail` (the same fetch the current scenario already uses) and answer from that response. NEVER restate a balance, available, pending, or total from earlier conversation context — balances move between turns. This stacks with the existing guards (it is the read-side counterpart to the Post-Mutation and off-platform Freshness Guards) and obeys the same anti-over-fetch limit: if you already fetched the data **earlier in this same turn**, reuse that; an in-flow choice that cannot change server state (picking a list item, entering an amount, confirming) is not a re-fetch trigger. The rule is about never sourcing a money figure from cross-turn memory, not about calling tools repeatedly within one turn. **The refresh is automatic and unconditional — never an opt-in, never caveated.** Re-fetch FIRST, then answer from the fresh response. You MUST NOT: (a) show a figure from earlier in the session and label it "from the account detail fetched earlier" / "as of the last lookup"; (b) ask the user whether to refresh or do a "live lookup" — that choice does not exist, the fetch is unconditional; (c) present stale numbers alongside an offer to refresh. The named anti-pattern to avoid: rendering an earlier balance and then asking "This is from earlier this session — would you like me to refresh it with a live lookup?" — that is wrong; just fetch and answer. The user asking for the balance IS the instruction to fetch; the only thing that defers the fetch is a Token Guard `success:false` + `authPageUrl` (→ login), never a "want me to refresh?" prompt.

**(2) Per-currency total — quote ONLY the API's `totalAmountDisplay` field.** `get_account_detail`'s `instrumentInfoMap[{currency}]` carries an authoritative `totalAmountDisplay` (USD). When the user asks a single currency's total, read that field directly — do NOT compute `available + pending` yourself, do NOT reuse a context value. If the field is absent or `0` for a currency, render its total as `—` rather than fabricating one. (Account-type-level totals come from the Account Overview's own `totalAmount`, unchanged.)

The Token Guard still takes priority: a fetch returning `success:false` + `authPageUrl` → Token Guard, not a balance answer.

## Wealth Evaluation Gate — Mandatory Pre-Closing Check (money branch, non-wealth scenarios)

Applies whenever an Account Overview has been rendered in this response AND the current scenario is **withdraw / swap** (NOT deposit, NOT wealth). Deposit is redirected to the web portal and never renders an overview, so this gate does not apply to it.

Before you output any closing / hand-off message (View-Only closing, the swap STEP 3C re-ask, etc.), you MUST have:

- **Step A** — evaluated ALL 5 conditions of `WEALTH_RECOMMENDATION_TRIGGER` (`references/shared/wealth-recommendation.md`). This evaluation is mandatory and happens BEFORE any tool call. There is NO path that skips it.
- **Step B** — branched:
  - All 5 TRUE → call `investor_precheck`, then follow the recommendation flow in that file.
  - Any FALSE → record which condition (1–5, incl. 5a/5b/5c) was FALSE and why; do NOT call `investor_precheck`; proceed to the closing message.

⛔ There is no legitimate path where `investor_precheck` is called without first confirming all 5 conditions TRUE. Calling it "just in case" when condition 5 is FALSE (clear business intent) is a rule violation — it wastes a call and may show an unwanted recommendation.

⛔ **The recommendation is product-catalog-or-nothing — there is no teaser.** The post-balance wealth recommendation (withdraw / swap / deposit scenarios) renders the product catalog (Rich Recommendation) ONLY when `investor_precheck` was invoked THIS turn AND returned **every** value `true`. In every other case — any `false`, not called, errored, Token Guard, or skipped → render **nothing**. You MUST NOT output any "去完成投资者声明 / 去完成签署 / complete your investor declaration / complete the signing" eligibility text in a recommendation context; that wording belongs ONLY to the wealth scenario the user explicitly enters (`references/wealth/wealth.md` STEP 2). Emitting such a block as a default or from memory is a rule violation. (Scenario-agnostic: applies to withdraw / swap / deposit money branches alike.)

**Self-check before sending:** if your draft contains a recommendation block BUT the original message had clear business intent → INVALID, condition 5 should have been FALSE: remove it. If your draft contains the closing sentence BUT you never evaluated the 5 conditions → INVALID: go back to Step A. If your draft contains any "去完成签署 / complete your investor declaration / complete the signing" eligibility text in a recommendation context → INVALID: remove it (the recommendation is product-catalog-or-nothing; that wording belongs only to the explicit wealth scenario). "Evaluation mandatory, render non-blocking" — the *render* never halts the flow; the *evaluation* is never skippable.

## Language

Detect the dominant language of the user's latest message and use it consistently for the ENTIRE turn — reasoning, tool-call preambles, tool-parameter descriptions, and the final reply. Judge each turn independently; switch the moment the user switches. For mixed-language messages, pick the dominant language by character count; near-ties default to English. Currency codes (USD, USDT, BTC…) and server-returned strings stay verbatim regardless of language.

## Branding

- Money scenarios (deposit / withdraw / swap / wealth) → always say **MetaComp**.
- Never say "MCP server" or "the server" alone.

## Destination Source of Truth — addresses ALWAYS come from the system

**Same priority as Token Guard.** For ANY money-out operation (crypto
withdrawal, fiat withdrawal), the destination — crypto wallet address or
beneficiary bank account — is **pre-registered in MetaComp and MUST be
fetched from the system, then picked by the user from a list**
(`get_crypto_withdrawal_wallets` / `get_fiat_withdrawal_bank_accounts`).
The user **never types, pastes, or dictates** a destination, and you never
use one they volunteer — first-party AND third-party alike.

Three rationalizations are explicitly **forbidden** — they are the exact
ways this rule gets broken:

1. **"No first-party wallet on file, so I'll accept a typed address."** No.
   An empty first-party list means *register one in the dashboard, or
   cancel* — never grounds to accept a typed address.
2. **"I'll process the typed address as a third-party withdrawal
   instead."** No. Switching party type does NOT unlock typed addresses —
   third-party beneficiaries also come from the registered list. An
   unregistered beneficiary must be added in the dashboard first.
3. **"The registered wallet is on a different network than they want, so
   there's effectively no usable wallet."** No. A wallet on another network
   is NOT an empty list — show it. Network compatibility is resolved by the
   dedicated compat step, not by hiding wallets or accepting a typed one.

Before sending any reply, re-read your draft: if it offers the user the
option to type/provide an address, or pivots first-party→third-party to
accommodate a typed address, the draft is a rule violation — discard it and
either show the fetched list or, if it is genuinely empty, the
register-in-dashboard message.

## Funds-First Gate — verify the balance covers the spend BEFORE asking the user to commit

**Same priority as Token Guard.** Applies to every money-out write —
`execute_crypto_withdrawal`, `execute_fiat_withdrawal`, `fip_subscribe`, and
swap `confirm_otc_trade`. The principle: **a user should never be walked
through wallet/account selection, a confirmation card, and especially a
6-digit verification code only to have the transaction rejected for
insufficient balance at execution.** Checking funds is cheap; the backend
already knows the available balance. Surface a shortfall as early as the
flow can know the currency and amount, and never later than the moment
before you ask the user to commit.

Two checkpoints, both required:

1. **Early zero-balance check — right after the currency is fixed.** As soon
   as the withdrawal/subscription currency is known (before bank/wallet
   selection, before asking for an amount), look up the available balance
   for that currency in the funding account. If it is `0` or the currency is
   absent from the account, stop right there and let the user pick another
   currency or cancel — do not march them deeper into a flow that cannot
   succeed.

2. **Sufficiency gate — before the confirmation card / verification code.**
   You MUST have an authoritative confirmation that `available ≥ the total
   amount debited` (NOT merely the amount the recipient receives — for a
   third-party withdrawal the fee is added on top, so compare the *total
   debit*). For withdrawals, `get_withdrawal_quote` now returns
   `balance.sufficient` (and `balance.availableAmount`) computed against the
   total debit — read it directly: `false` → stop and tell the user the
   shortfall before any confirmation card or code. For FIP/swap, fetch the
   funding-account balance and compare yourself.

**Freshness — never gate on a stale number.** Balances move, and a session
may have re-logged-in mid-flow (Token Guard). Do not trust a balance you
rendered many turns ago. If the authoritative source is unavailable
(`get_withdrawal_quote` returned `balance: null`, or you can't point to a
balance fetched *after* the last login/gap), re-fetch `get_account_detail`
for the funding account and gate on that — don't wave the transaction
through on an assumption.

**Self-check before any reply that renders a confirmation card or asks for a
verification code:** can you point to a fresh `available ≥ total debit`
result? If not, your draft is INVALID — go back, fetch/read the balance, and
either stop on the shortfall or proceed only once it clears. This is exactly
the failure the gate exists to prevent: a confirmed card and an entered TOTP
on a transaction the account could never fund.

## Amount Sanity Gate — every amount is validated the moment it enters the flow, from ANY source

**Same priority as Token Guard.** Applies to every scenario that carries a money
amount — withdraw, wealth/FIP subscription, swap, third-party deposit.

**Where the amount came from is irrelevant.** This fires on the amount you are
about to *use*, not on the step that happened to collect it. All of these are in
scope:

- an amount supplied in the opening message ("withdraw -100 USDT", "申购 -500 USDT 理财")
- an amount typed at a scenario's own amount STOP
- an amount the user revises mid-flow ("make it 250 instead")
- an amount carried forward from an earlier turn in this session

**The check.** Normalize first (strip thousand separators `,`, trim whitespace),
then require BOTH:

1. it matches `^\d+(\.\d+)?$` — digits with at most one decimal point, nothing
   else: no leading `-` or `+`, no scientific notation, no currency symbol, no words
2. its numeric value is strictly greater than `0`

**On failure — re-ask, never repair.** You MUST NOT:

- ❌ strip the sign, take the absolute value, or otherwise "correct" the number.
  `-100` does NOT become `100`.
- ❌ infer what the user "must have meant" and proceed on that inference
- ❌ pass the value to any tool
- ❌ render it in a confirmation card, a quote, or any summary
- ❌ silently continue the flow with a substituted value

Instead, stop where you are and re-ask, naming what was wrong:

> The amount must be a positive number, e.g. "200" or "0.5". You entered {raw}.
> Please tell me the amount you want. / 金额必须是正数，例如 "200" 或 "0.5"。
> 您输入的是 {raw}，请重新告诉我金额。

Stay on the current step. Do NOT advance, do NOT call the next tool, and do NOT
treat the re-asked amount as confirmation of anything else.

**Self-check before any tool call that carries an amount, and before any card
that displays one:** did this exact value pass both conditions above in this
turn? If you cannot point to that, your draft is INVALID — go back and re-ask.

Scenario files may add stricter rules on top (minimum / maximum, decimal-place
limits, balance sufficiency). Those stack; none of them replaces this gate.

## Universal don'ts

- ❌ Do NOT fabricate wallet addresses, account numbers, bank details, or any financial data.
- ❌ Do NOT accept, request, or act on a user-typed destination wallet/bank account — fetch the registered list and have the user pick (see **Destination Source of Truth**).
- ❌ Do NOT skip any ⛔ STOP point — every STOP waits for user input.
- ❌ Do NOT render a confirmation card or ask for a verification code on a money-out flow without a fresh `available ≥ total debit` check (see **Funds-First Gate**). Discovering insufficient funds at execution — after the user committed and entered a code — is the exact bug this prevents.
- ❌ Do NOT "repair" a malformed amount — dropping a minus sign, taking the absolute value, or guessing the intended number. Re-ask instead (see **Amount Sanity Gate**). Silently turning `-100` into `100` and rendering a confirmation card is the exact bug this prevents.
- ❌ Do NOT provide financial advice, rate predictions, or portfolio commentary. These skills handle transaction/analysis mechanics only.
- ❌ **Do NOT call `get_module_permissions` in any scenario.** Module-permission entitlements are **out of scope** for every flow in this skill — none of them gate on module permissions. Even though the tool appears in your `metacomp-mcp` tool list, **treat it as unavailable**: never invoke it, and never let your own planning insert a "check module permissions" step. If you think you need it, you don't — proceed with the scenario's own steps.
- ✅ All amounts display with thousands separators (e.g. `10,000` not `10000`).

---

# Scenario map

| Scenario | Entry file | What it does |
|---|---|---|
| deposit | _(no file — redirect)_ | Redirect the user to the MetaComp web portal `https://camp.mce.sg/`; see "Deposit — redirected to web portal" |
| withdraw | `references/withdraw/withdraw.md` | Fiat / crypto withdrawal + withdrawal history |
| accounts | `references/withdraw/withdraw.md` (Account Roster section) | Read-only list of registered bank accounts, grouped + labeled by first-party / third-party / same-name |
| swap | `references/swap/swap.md` | OTC currency exchange (lock quote → confirm → execute) |
| swap-history | `references/swap/swap-history.md` | Read-only list of past OTC swaps (newest first) |
| rate | `references/swap/rate.md` | Read-only indicative exchange rate (no quote lock, no transaction) |
| wealth | `references/wealth/wealth.md` | FIP subscription (precheck → catalog → agreement → subscribe) |

Shared (money branch only): `references/shared/{auth-kyc-setup,account-overview,wealth-recommendation}.md`.

