diff --git a/aastar-frontend/app/community/credibility/page.tsx b/aastar-frontend/app/community/credibility/page.tsx
new file mode 100644
index 0000000..2c0c6a8
--- /dev/null
+++ b/aastar-frontend/app/community/credibility/page.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+/**
+ * Community xPNTs economic-credibility disclosure (CC-33).
+ *
+ * Lists every community xPNTs token with its on-chain credibility score (0–100
+ * backing coverage) and an over-issue warning, then discloses the audit/slash
+ * rules behind the score (sourced from DVT `docs/AUDIT_SLASH_MODEL.md`). Pure
+ * read-only, client-side: data comes from `@aastar/sdk` typed views via
+ * `lib/sdk/credibility`, no wallet/signing required.
+ */
+import { useCallback, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { formatUnits } from "viem";
+import {
+ ShieldCheckIcon,
+ ExclamationTriangleIcon,
+ ArrowPathIcon,
+} from "@heroicons/react/24/outline";
+import Layout from "@/components/Layout";
+import { listCommunityCredibility, type CommunityCredibility } from "@/lib/sdk/credibility";
+
+/** USD fields are 18-decimal fixed point (USD × 1e18) — format, don't treat as wei. */
+function formatUsd(value: bigint): string {
+ const whole = formatUnits(value, 18);
+ const n = Number(whole);
+ if (!Number.isFinite(n)) return whole;
+ return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
+}
+
+/** Score → semantic color band (score is a backing-coverage %). */
+function scoreBand(score: number): { bar: string; text: string } {
+ if (score >= 80) return { bar: "bg-green-500", text: "text-green-700 dark:text-green-400" };
+ if (score >= 50) return { bar: "bg-amber-500", text: "text-amber-700 dark:text-amber-400" };
+ return { bar: "bg-red-500", text: "text-red-700 dark:text-red-400" };
+}
+
+function shortenAddr(addr: string): string {
+ return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
+}
+
+function CredibilityRow({ entry }: { entry: CommunityCredibility }) {
+ const { t } = useTranslation();
+ const { credibility: c } = entry;
+ const score = Math.max(0, Math.min(100, c.credibilityScore));
+ const band = scoreBand(score);
+
+ return (
+
+
+
+
+ {entry.name}
+ {entry.symbol}
+
+
+ {shortenAddr(entry.token)}
+
+
+ {c.isOverIssued && (
+
+
+ {t("credibilityPage.overIssuedBadge")}
+
+ )}
+
+
+
+
+
+ {t("credibilityPage.scoreLabel")}
+
+ {score}/100
+
+
+
+
+
+
+
- {t("credibilityPage.backing")}
+ - ${formatUsd(c.backingValueUSD)}
+
+
+
- {t("credibilityPage.issued")}
+ - ${formatUsd(c.issuedValueUSD)}
+
+
+
- {t("credibilityPage.cap")}
+ - ${formatUsd(c.effectiveCapUSD)}
+
+
+
+ {c.isOverIssued && (
+
+ {t("credibilityPage.overIssuedHint")}
+
+ )}
+
+ );
+}
+
+function RulesDisclosure() {
+ const { t } = useTranslation();
+ const penalties = t("credibilityPage.rules.penalties", { returnObjects: true }) as Array<{
+ name: string;
+ desc: string;
+ }>;
+ const ruleList = t("credibilityPage.rules.ruleList", { returnObjects: true }) as string[];
+
+ return (
+
+
+
+ {t("credibilityPage.rules.title")}
+
+ {t("credibilityPage.rules.intro")}
+
+
+ {Array.isArray(penalties) &&
+ penalties.map((p) => (
+
+ {p.name}
+ — {p.desc}
+
+ ))}
+
+
+
+ {t("credibilityPage.rules.formulaTitle")}
+
+ {t("credibilityPage.rules.formula")}
+
+ {Array.isArray(ruleList) && ruleList.length > 0 && (
+
+ {ruleList.map((r, i) => (
+ - {r}
+ ))}
+
+ )}
+
+ {t("credibilityPage.rules.source")}
+
+ );
+}
+
+export default function CredibilityPage() {
+ const { t } = useTranslation();
+ const [entries, setEntries] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const rows = await listCommunityCredibility();
+ setEntries(rows.filter((r): r is CommunityCredibility => r !== null));
+ } catch (e) {
+ setError(e instanceof Error ? e.message : String(e));
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ return (
+
+
+
+
+
{t("credibilityPage.title")}
+
{t("credibilityPage.subtitle")}
+
+
+
+
+
+ {loading && (
+
{t("credibilityPage.loading")}
+ )}
+ {!loading && error && (
+
+ {t("credibilityPage.error")}: {error}
+
+ )}
+ {!loading && !error && entries.length === 0 && (
+
{t("credibilityPage.empty")}
+ )}
+ {!loading &&
+ !error &&
+ entries.map((entry) =>
)}
+
+
+
+
+
+ );
+}
diff --git a/aastar-frontend/app/community/page.tsx b/aastar-frontend/app/community/page.tsx
index 4bdf3f9..9e944f4 100644
--- a/aastar-frontend/app/community/page.tsx
+++ b/aastar-frontend/app/community/page.tsx
@@ -334,6 +334,29 @@ export default function CommunityPage() {
)}
+ {/* Credibility disclosure entry */}
+
+
+
+
{/* Community List */}
diff --git a/aastar-frontend/lib/i18n/locales/en.json b/aastar-frontend/lib/i18n/locales/en.json
index d3dea16..e5637b1 100644
--- a/aastar-frontend/lib/i18n/locales/en.json
+++ b/aastar-frontend/lib/i18n/locales/en.json
@@ -713,6 +713,39 @@
"loadError": "Failed to load registry data",
"queryError": "Failed to query role for address"
},
+ "credibilityPage": {
+ "title": "Community xPNTs Credibility",
+ "subtitle": "The on-chain credibility score (0–100) and over-issue status of each community's xPNTs. Computed live from on-chain economic facts — no manual input.",
+ "refresh": "Refresh",
+ "loading": "Reading on-chain credibility…",
+ "error": "Failed to load",
+ "empty": "No community xPNTs deployed yet",
+ "overIssuedBadge": "Over-issued",
+ "overIssuedHint": "This community has issued more xPNTs value than its backing collateral — circulating value exceeds redeemable value, so holdings carry depreciation risk.",
+ "scoreLabel": "Credibility score (backing coverage)",
+ "backing": "Backing (USD)",
+ "issued": "Issued (USD)",
+ "cap": "Cap (USD)",
+ "rules": {
+ "title": "Where does the score come from? Audit & slash rules",
+ "intro": "The credibility score is a purely informational on-chain reputation signal (not a penalty). The DVT validator network has three independent, never-conflated actions on nodes:",
+ "penalties": [
+ { "name": "SLASH (burn stake)", "desc": "Burns the operator's stake, irreversible. Only for objective, attributable, globally-verifiable on-chain economic fraud, decided by BLS-quorum consensus." },
+ { "name": "JAIL (exclude)", "desc": "Stops fee and removes the node from the active set; self-heals. For objective on-chain liveness (offline) facts, auto-enforced by SuperPaymaster." },
+ { "name": "REPUTATION (score)", "desc": "This public credibility score itself — no penalty, purely informational, computed live from on-chain views." }
+ ],
+ "formulaTitle": "Credibility score formula (computed live on-chain)",
+ "formula": "score = min(100, backingValue ÷ issuedValue × 100), i.e. backing coverage %; flagged over-issued when issuedValue > effectiveCap. All three USD figures are 18-decimal fixed-point accounting units.",
+ "ruleList": [
+ "Over-issue is disclosed only as an on-chain credibility score, never slashed — the score and over-issued flag are computed by the contract and anyone can verify them directly on-chain.",
+ "Effective cap = per-industry baseline (factory-governed; a community can't self-pick a high baseline) × cap ratio + backing value.",
+ "Backing = the community's aPNTs staked in SuperPaymaster (redemption services via MyShop may be added later).",
+ "Going offline only jails (fee-stop / active-set removal), never slashes stake; an individual over their spend limit is refused at sign time, which also never slashes the operator's stake.",
+ "Forged slash proposals (proof-forgery) are not enforced — co-sign re-verification and on-chain aggregate rejection already block forgeries; residual spam falls to reputation."
+ ],
+ "source": "Basis: DVT repo docs/AUDIT_SLASH_MODEL.md (CC-28 on-chain credibility views + the corrected four-rule model)."
+ }
+ },
"communityPage": {
"title": "Community Portal",
"loadError": "Failed to load community data",
diff --git a/aastar-frontend/lib/i18n/locales/zh.json b/aastar-frontend/lib/i18n/locales/zh.json
index e4b34c1..db251a1 100644
--- a/aastar-frontend/lib/i18n/locales/zh.json
+++ b/aastar-frontend/lib/i18n/locales/zh.json
@@ -713,6 +713,39 @@
"loadError": "加载注册表数据失败",
"queryError": "查询该地址角色失败"
},
+ "credibilityPage": {
+ "title": "社区 xPNTs 可信度披露",
+ "subtitle": "每个社区积分(xPNTs)的链上可信度分(0–100)与是否超发。分数实时由链上经济事实自动算出,无人工干预。",
+ "refresh": "刷新",
+ "loading": "读取链上可信度…",
+ "error": "加载失败",
+ "empty": "暂无已部署的社区 xPNTs",
+ "overIssuedBadge": "已超发",
+ "overIssuedHint": "该社区发行的 xPNTs 价值已超过其抵押背书 —— 实际流通 > 可兑付价值,持有存在贬值风险。",
+ "scoreLabel": "可信度分(背书覆盖率)",
+ "backing": "背书 (USD)",
+ "issued": "已发行 (USD)",
+ "cap": "发行上限 (USD)",
+ "rules": {
+ "title": "分数怎么来的?审计与惩罚规则",
+ "intro": "可信度分是纯信息性的链上声誉指标(不是惩罚)。DVT 验证网络对节点有三类处置,各自独立、切勿混淆:",
+ "penalties": [
+ { "name": "SLASH(罚没质押)", "desc": "烧掉运营者的质押,不可逆。仅针对客观、可归因、全网可验证的链上经济欺诈,需 BLS 多签共识。" },
+ { "name": "JAIL(隔离)", "desc": "停止计费并移出活跃集,可自愈。针对客观的链上掉线事实,由 SuperPaymaster 自动执行。" },
+ { "name": "REPUTATION(声誉)", "desc": "就是这个公开的可信度分,不含任何惩罚,纯信息披露,由链上视图实时计算。" }
+ ],
+ "formulaTitle": "可信度分公式(链上实时计算)",
+ "formula": "可信度分 = min(100, 背书价值 ÷ 已发行价值 × 100),即背书覆盖率百分比;当「已发行价值 > 有效上限」时标记为已超发。三项 USD 数值均为 18 位精度定点记账。",
+ "ruleList": [
+ "超发只作为链上可信度分披露,不触发罚没 —— 分数与「已超发」标记由合约自动算出,任何人可直接读链核对。",
+ "有效上限 = 行业基线(工厂治理,社区不可自选高基线)× 上限系数 + 背书价值。",
+ "背书 = 社区在 SuperPaymaster 质押的 aPNTs(未来可加 MyShop 兑付服务)。",
+ "掉线只隔离(停计费/移出活跃集)不罚质押;个人超额消费在签名前被拒,也不罚运营者质押。",
+ "伪造罚没提案(proof-forgery)暂未启用 —— 联签复验 + 链上聚合拒绝已能挡住伪造,残余刷屏归入声誉分。"
+ ],
+ "source": "规则依据:DVT 仓 docs/AUDIT_SLASH_MODEL.md(CC-28 链上可信度视图 + 修正后的四规则模型)。"
+ }
+ },
"communityPage": {
"title": "社区门户",
"loadError": "加载社区数据失败",
diff --git a/aastar-frontend/lib/sdk/credibility.ts b/aastar-frontend/lib/sdk/credibility.ts
new file mode 100644
index 0000000..38dbc47
--- /dev/null
+++ b/aastar-frontend/lib/sdk/credibility.ts
@@ -0,0 +1,126 @@
+/**
+ * xPNTs economic-credibility disclosure — SDK boundary (CC-33).
+ *
+ * A community's xPNTs token exposes an on-chain, auto-computed credibility
+ * snapshot (CC-28, SP-side): a 0–100 backing-coverage score plus the raw USD
+ * accounting figures behind it. This module is a thin, read-only wrapper over the
+ * @aastar/sdk 0.43.0 (`@aastar/sdk/core`) typed views — no signing, no writes —
+ * so the disclosure page (`app/community/credibility`) only wires UI.
+ *
+ * - `xPNTsFactoryActions(factory)(client).getAllTokens()` enumerates the deployed
+ * community tokens (factory address from the SDK canonical table, never hardcoded).
+ * - `xPNTsTokenActions()(client).getCredibility({ token })` reads all five views
+ * pinned to a single block, so the snapshot is self-consistent.
+ *
+ * The three `*ValueUSD` fields are **18-decimal fixed-point USD** (USD × 1e18),
+ * NOT wei — verified on-chain (SDK CC-33): `formatUnits(v, 18)` for display,
+ * raw `bigint` compares for the over-issue verdict (no float).
+ *
+ * @module lib/sdk/credibility
+ */
+import { erc20Abi } from "viem";
+import type { Address, PublicClient } from "viem";
+import {
+ CHAIN_SEPOLIA,
+ getCanonicalAddresses,
+ xPNTsFactoryActions,
+ xPNTsTokenActions,
+ type Credibility,
+} from "@aastar/sdk/core";
+import { ensureSdkConfig, getPublicClient } from "./client";
+
+export type { Credibility };
+
+/** A community xPNTs token plus its display metadata and credibility snapshot. */
+export interface CommunityCredibility {
+ /** The xPNTs token contract address. */
+ token: Address;
+ /** ERC-20 name (falls back to the short address if the read reverts). */
+ name: string;
+ /** ERC-20 symbol (falls back to "xPNTs" if the read reverts). */
+ symbol: string;
+ /** Self-consistent on-chain credibility snapshot (5 views pinned to one block). */
+ credibility: Credibility;
+}
+
+/** Canonical xPNTs factory for the configured chain (from the SDK, not hardcoded). */
+export function getXPNTsFactory(chainId: number = CHAIN_SEPOLIA): Address {
+ ensureSdkConfig(chainId);
+ const factory = getCanonicalAddresses(chainId)?.xPNTsFactory;
+ if (!factory) {
+ throw new Error(`No canonical xPNTs factory for chain ${chainId}`);
+ }
+ return factory as Address;
+}
+
+/** Enumerate every community xPNTs token deployed by the canonical factory. */
+export async function listCommunityTokens(
+ client?: PublicClient,
+ chainId: number = CHAIN_SEPOLIA
+): Promise {
+ ensureSdkConfig(chainId);
+ const pc = client ?? getPublicClient();
+ return xPNTsFactoryActions(getXPNTsFactory(chainId))(pc).getAllTokens();
+}
+
+/**
+ * One-shot credibility snapshot for a token — the five credibility views batched
+ * at a single block so `credibilityScore` can't drift out of sync with the USD
+ * figures. Optionally pin a specific block for deterministic/historical reads.
+ */
+export async function getCredibility(
+ token: Address,
+ client?: PublicClient,
+ chainId: number = CHAIN_SEPOLIA,
+ blockNumber?: bigint
+): Promise {
+ ensureSdkConfig(chainId);
+ const pc = client ?? getPublicClient();
+ return xPNTsTokenActions()(pc).getCredibility(
+ blockNumber === undefined ? { token } : { token, blockNumber }
+ );
+}
+
+/** Short 0x…abcd form for display fallbacks. */
+function shortAddr(addr: Address): string {
+ return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
+}
+
+/** Read ERC-20 name/symbol for a token, tolerating non-standard tokens. */
+export async function getTokenMeta(
+ token: Address,
+ client?: PublicClient
+): Promise<{ name: string; symbol: string }> {
+ const pc = client ?? getPublicClient();
+ const [name, symbol] = await Promise.all([
+ pc.readContract({ address: token, abi: erc20Abi, functionName: "name" }).catch(() => shortAddr(token)),
+ pc.readContract({ address: token, abi: erc20Abi, functionName: "symbol" }).catch(() => "xPNTs"),
+ ]);
+ return { name, symbol };
+}
+
+/**
+ * Full disclosure feed: every community token with its metadata + credibility.
+ * A single failed token is surfaced as `null` so one bad token can't blank the
+ * whole page; callers filter it out.
+ */
+export async function listCommunityCredibility(
+ chainId: number = CHAIN_SEPOLIA
+): Promise> {
+ ensureSdkConfig(chainId);
+ const pc = getPublicClient();
+ const tokens = await listCommunityTokens(pc, chainId);
+ return Promise.all(
+ tokens.map(async (token): Promise => {
+ try {
+ const [meta, credibility] = await Promise.all([
+ getTokenMeta(token, pc),
+ getCredibility(token, pc, chainId),
+ ]);
+ return { token, name: meta.name, symbol: meta.symbol, credibility };
+ } catch {
+ return null;
+ }
+ })
+ );
+}