@seasonkoh/webaz 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/money.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Money — 协议资金的【唯一】算术面(RFC-014 option A:整数最小单位)。
3
+ *
4
+ * 背景:此前金额用 JS float(SQLite REAL + number + Math.round(x*100)/100),除不尽的金额
5
+ * (33.33 @ 7% 等)会在【个体钱包】留浮点尘(staked=3.3299999999983)。聚合守恒安全(残差0,
6
+ * 靠 residual-absorption),但接真实 USDC(整数 base-unit)对账时会变成真裂缝。诊断见
7
+ * tests/test-money-precision-adversarial.ts。
8
+ *
9
+ * 表示:1 WAZ = 1,000,000 base-units(6 位小数,与 USDC base-units 对齐 → 链上对账 1:1)。
10
+ * 全部算术在【整数 base-units】上做(精确),只在显示层转回 2 位小数。
11
+ * 类型用 number 整数(安全到 ±2^53 ≈ 9.007e9 WAZ,模拟币足够);链上 USDC 边界 viem 已用 BigInt。
12
+ *
13
+ * 守恒利器:allocate() 用最大余数法把总额拆成整数桶且【精确求和 = 总额】——从源头消灭 dust,
14
+ * 且天然守恒(不增发/不丢)。mulRate() 单次舍入到整数单位。
15
+ *
16
+ * 用法(RFC-014 港口策略):资金热路径(engine.settle* / orders / 佣金 / dispute / skill …)
17
+ * 一律 toUnits 进 → 在 units 上 add/sub/mulRate/allocate → toUnits 出存库;显示层用 format。
18
+ * 全部港口完成后 schema 改 INTEGER(PR6),届时 toUnits/toDecimal 退化为存取边界。
19
+ */
20
+ export const MONEY_SCALE = 1_000_000; // 1 WAZ = 1e6 base-units(6 dp,USDC 对齐)
21
+ const MAX_SAFE = Number.MAX_SAFE_INTEGER; // 2^53 - 1 ≈ 9.007e15 units ≈ 9.007e9 WAZ
22
+ function assertUnits(u, ctx) {
23
+ if (!Number.isFinite(u) || !Number.isInteger(u))
24
+ throw new Error(`money[${ctx}]: 非整数 units: ${u}`);
25
+ if (Math.abs(u) > MAX_SAFE)
26
+ throw new Error(`money[${ctx}]: units 超出安全整数范围: ${u}`);
27
+ }
28
+ /** 十进制 WAZ(number/string)→ 整数 base-units(四舍五入到最近 unit)。存量 REAL 价格的入口。 */
29
+ export function toUnits(decimal) {
30
+ const n = typeof decimal === 'string' ? Number(decimal) : decimal;
31
+ if (!Number.isFinite(n))
32
+ throw new Error(`money.toUnits: 非有限数: ${decimal}`);
33
+ const u = Math.round(n * MONEY_SCALE);
34
+ assertUnits(u, 'toUnits');
35
+ return u;
36
+ }
37
+ /** 整数 base-units → 十进制 number(兼容/显示边界;返回 float,仅用于显示或与遗留 REAL 字段衔接)。 */
38
+ export function toDecimal(units) {
39
+ assertUnits(units, 'toDecimal');
40
+ return units / MONEY_SCALE;
41
+ }
42
+ /** 整数 base-units → 定点字符串(默认 2 位,WAZ 展示)。整数运算舍入,不靠 float toFixed。 */
43
+ export function format(units, dp = 2) {
44
+ assertUnits(units, 'format');
45
+ const neg = units < 0;
46
+ let abs = Math.abs(units);
47
+ const grain = Math.round(MONEY_SCALE / 10 ** dp); // dp=2 → 1e4 units 为一格
48
+ abs = Math.round(abs / grain) * grain; // 舍入到 dp 粒度(整数运算)
49
+ const whole = Math.floor(abs / MONEY_SCALE);
50
+ const fracUnits = abs - whole * MONEY_SCALE;
51
+ const fracStr = dp > 0 ? '.' + String(Math.floor(fracUnits / grain)).padStart(dp, '0') : '';
52
+ return (neg ? '-' : '') + String(whole) + fracStr;
53
+ }
54
+ export function add(a, b) { const r = a + b; assertUnits(r, 'add'); return r; }
55
+ export function sub(a, b) { const r = a - b; assertUnits(r, 'sub'); return r; }
56
+ export function sum(xs) { return xs.reduce((acc, x) => add(acc, x), 0); }
57
+ /** 单价 × 整数数量(精确)。 */
58
+ export function mulQty(unit, qty) {
59
+ if (!Number.isInteger(qty) || qty < 0)
60
+ throw new Error(`money.mulQty: qty 非非负整数: ${qty}`);
61
+ const r = unit * qty;
62
+ assertUnits(r, 'mulQty');
63
+ return r;
64
+ }
65
+ /** 金额 × 费率(rate 如 0.07);结果【单次】舍入到整数 base-units。守恒由 allocate 负责,勿用多次 mulRate 凑总额。 */
66
+ export function mulRate(amount, rate) {
67
+ if (!Number.isFinite(rate) || rate < 0)
68
+ throw new Error(`money.mulRate: rate 非法: ${rate}`);
69
+ assertUnits(amount, 'mulRate');
70
+ const r = Math.round(amount * rate);
71
+ assertUnits(r, 'mulRate');
72
+ return r;
73
+ }
74
+ /** clamp 到 [0, cap](cap 省略=只防负)。用于"不超过余额/不超过原佣金"等上限。 */
75
+ export function clamp(units, lo = 0, hi = MAX_SAFE) {
76
+ assertUnits(units, 'clamp');
77
+ return Math.max(lo, Math.min(hi, units));
78
+ }
79
+ /**
80
+ * 把 total 按整数权重拆成 N 桶,【精确求和 = total】(最大余数法)。
81
+ * - 守恒:Σ 输出 === total,无 dust、不增发/不丢。
82
+ * - 余数(floor 后差额)按小数部分从大到小逐 1 派发(确定性、稳定)。
83
+ * - 全 0 权重 → 全 0 桶(调用方决定 total 的去向,通常落 reserve)。
84
+ * 用于佣金/罚没/分润等"一笔总额分给多方"的场景,替代逐项 round 造成的不守恒。
85
+ */
86
+ export function allocate(total, weights) {
87
+ assertUnits(total, 'allocate');
88
+ if (weights.some(w => !Number.isFinite(w) || w < 0))
89
+ throw new Error(`money.allocate: 权重含负/非法`);
90
+ const W = weights.reduce((a, b) => a + b, 0);
91
+ if (W <= 0)
92
+ return weights.map(() => 0);
93
+ const raw = weights.map(w => (total * w) / W);
94
+ const floor = raw.map(Math.floor);
95
+ const used = floor.reduce((a, b) => a + b, 0);
96
+ let remainder = total - used; // 整数,0..N-1(total≥0 时)
97
+ const out = floor.slice();
98
+ const order = raw.map((r, i) => ({ i, frac: r - Math.floor(r) })).sort((a, b) => b.frac - a.frac || a.i - b.i);
99
+ for (let k = 0; remainder > 0 && k < order.length; k++, remainder--)
100
+ out[order[k].i] += 1;
101
+ // total 为负数的极端情况(理论上不该发生)兜底:把残余补到最后一桶,守恒优先
102
+ if (remainder !== 0)
103
+ out[out.length - 1] += remainder;
104
+ out.forEach((u, i) => assertUnits(u, `allocate[${i}]`));
105
+ return out;
106
+ }
@@ -0,0 +1,103 @@
1
+ const BASE = 'https://webaz.xyz';
2
+ // ACP availability 枚举(spec):in_stock | out_of_stock | pre_order | backorder | unknown
3
+ function availability(stock) {
4
+ const n = Number(stock);
5
+ if (!Number.isFinite(n))
6
+ return 'unknown';
7
+ return n > 0 ? 'in_stock' : 'out_of_stock';
8
+ }
9
+ // 绝对化图片 URL(相对路径 → https://webaz.xyz/...);已是 http(s) 的原样返回
10
+ function absolutizeImg(raw) {
11
+ if (typeof raw !== 'string' || !raw.trim())
12
+ return null;
13
+ const s = raw.trim();
14
+ if (/^https?:\/\//i.test(s))
15
+ return s;
16
+ return BASE + (s.startsWith('/') ? s : '/' + s);
17
+ }
18
+ // description 必须 plain text(spec)→ 去 HTML 标签 + 折叠空白 + 截 5000
19
+ function plainText(raw, max) {
20
+ if (typeof raw !== 'string')
21
+ return '';
22
+ return raw.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
23
+ }
24
+ export function buildAcpProductFeed(db, opts = {}) {
25
+ const limit = Math.min(Math.max(opts.limit ?? 2000, 1), 5000);
26
+ let rows = [];
27
+ try {
28
+ rows = db.prepare(`
29
+ SELECT p.id, p.title, p.description, p.price, p.currency, p.stock, p.category,
30
+ p.images, p.brand, p.model, p.return_days, p.product_type,
31
+ p.seller_id, u.name AS seller_name
32
+ FROM products p
33
+ LEFT JOIN users u ON u.id = p.seller_id
34
+ WHERE p.status = 'active'
35
+ ORDER BY p.created_at DESC
36
+ LIMIT ?
37
+ `).all(limit);
38
+ }
39
+ catch {
40
+ rows = [];
41
+ }
42
+ const products = rows.map((p) => {
43
+ let imgs = [];
44
+ try {
45
+ const arr = JSON.parse(p.images || '[]');
46
+ if (Array.isArray(arr))
47
+ imgs = arr.map(absolutizeImg).filter((x) => !!x);
48
+ }
49
+ catch { /* malformed → no images */ }
50
+ const returnDays = Number(p.return_days);
51
+ const hasReturn = Number.isFinite(returnDays) && returnDays > 0;
52
+ const item = {
53
+ item_id: p.id, // 稳定商品 ID(spec: max 100)
54
+ title: (p.title || '').slice(0, 150), // spec: max 150
55
+ description: plainText(p.description, 5000), // spec: plain text, max 5000
56
+ url: `${BASE}/#order-product/${p.id}`, // 商品详情页(SPA hash,200)
57
+ // price: spec = Number + ISO 4217 code。WAZ 非 ISO 4217 → 见 feed 级 _disclosures.currency
58
+ price: { amount: Number(p.price), currency: p.currency || 'WAZ' },
59
+ availability: availability(p.stock),
60
+ is_digital: p.product_type === 'digital',
61
+ seller_name: (p.seller_name || p.seller_id).slice(0, 70),
62
+ seller_url: `${BASE}/#u/${p.seller_id}`,
63
+ // 诚实门控:可发现 ≠ 可经 ACP 购买(ACP /complete 是卡+PSP,WebAZ 未接 —— RFC-015)
64
+ is_eligible_search: true,
65
+ is_eligible_checkout: false,
66
+ is_eligible_ads: false,
67
+ };
68
+ if (imgs.length) {
69
+ item.image_url = imgs[0];
70
+ if (imgs.length > 1)
71
+ item.additional_image_urls = imgs.slice(1).join(',');
72
+ }
73
+ if (p.brand)
74
+ item.brand = String(p.brand).slice(0, 70);
75
+ if (p.model)
76
+ item.mpn = String(p.model).slice(0, 70);
77
+ if (p.category)
78
+ item.product_category = String(p.category);
79
+ if (hasReturn) {
80
+ item.accepts_returns = true;
81
+ item.return_deadline_in_days = Math.round(returnDays);
82
+ }
83
+ return item;
84
+ });
85
+ return {
86
+ feed_version: 1,
87
+ generated_at: new Date().toISOString(),
88
+ source: 'WebAZ',
89
+ spec: {
90
+ based_on: 'OpenAI Agentic Commerce Protocol — product feed',
91
+ api_version_observed: '2025-09-12',
92
+ reference: 'https://developers.openai.com/commerce/specs/feed',
93
+ rfc: `${BASE}/docs/INTEGRATOR.md`,
94
+ },
95
+ _disclosures: {
96
+ phase: 'pre-launch',
97
+ currency: "Each item's price.currency is the protocol's SIMULATED pre-launch internal unit (WAZ, or legacy 'DCP'), NOT an ISO 4217 fiat currency. No real money is involved.",
98
+ checkout: 'is_eligible_checkout is false for every item: ACP checkout is not yet wired. Products are DISCOVERABLE via ACP, not yet PURCHASABLE via ACP. Native WebAZ checkout + escrow + state machine govern real purchases (see RFC-015).',
99
+ },
100
+ product_count: products.length,
101
+ products,
102
+ };
103
+ }
@@ -22,7 +22,7 @@ export function buildEconomicParticipation(getParam) {
22
22
  note: 'RFC-011 §⑧. The roles by which an external actor enters the protocol VALUE flow (earns fees / posts collateral / bears conserved liability) — the highest liability tier (value_participant). Rates & thresholds are read LIVE from protocol_params at request time (doc=code: this index can never drift from the enforced economics). Honesty: roles marked status=live are enforced today; status=scaffolded means the hook exists but generic third-party onboarding awaits its own RFC + real demand (enters-core test).',
23
23
  principles: {
24
24
  fairness: ['public & transparent', 'liability follows the responsible party', 'zero cost to the faultless party'],
25
- conservation: 'Every settlement conserves value: a forfeit F is REDISTRIBUTED (protocol ≤ its fee, fund_base excluded / buyer up to 50% / promoters capped at original commission / residual commission_reserve) — never minted. See engine.settleFault.',
25
+ conservation: 'Every settlement conserves value: a forfeit F is REDISTRIBUTED (protocol ≤ its fee, fund_base excluded / promoters capped at their original commission / the harmed buyer gets 50% of the post-fee remainder AND absorbs any unused-commission residual so the buyer can exceed 50%) — never minted. See engine.settleFault.',
26
26
  bootstrap_no_forfeit: 'RFC-008: an order with stake_backing=0 (bootstrap / require_seller_stake=0) incurs ZERO forfeit and never touches the participant\'s free balance. Real forfeit applies only to staked orders.',
27
27
  },
28
28
  enter_value_flow: 'A value participant is in the accountability net via api_key→passport AND collateral/reputation-bound. Highest liability tier. See /.well-known/webaz-integration.json#liability_tiers.value_participant.',
@@ -15,10 +15,14 @@ export function buildIntegrationContract() {
15
15
  contract_version: CONTRACT_VERSION,
16
16
  software_version: SOFTWARE_VERSION,
17
17
  thesis: 'WebAZ is agent-native: you integrate by your agent reading this machine-readable contract and self-integrating — we do NOT build a bespoke API/auth/webhook layer per integrator. The protocol provides rules + semantics + boundaries + accountability + eventing + verifiability + settlement. See docs/RFC-011.',
18
+ // 源码仓库 launch 前私有 —— 公开声明,防"自称开源但 GitHub 404"被读成 vaporware。
19
+ source_status: 'The source repo (github.com/seasonsagents-art/webaz) is PRIVATE until the W8 public launch, so GitHub links in these surfaces may return 404 until then — they open at launch, not a dead project. The full machine-readable spec is ALREADY public via these /.well-known/* surfaces; an agent never needs the repo to integrate or verify.',
18
20
  // 外部 agent 的第一道问题:"我怎么从匿名读升到能写?" —— 入口必须自答(不依赖 GitHub)。
19
21
  access: {
22
+ browse_first: 'No account needed to START: browse the live catalog at https://webaz.xyz/#discover and read every well-known surface below anonymously. Try before you commit.',
20
23
  anonymous_read: 'no credential needed — public GET endpoints + the well-known surfaces below.',
21
24
  get_api_key: 'an api_key requires a REAL HUMAN to register at https://webaz.xyz (invite code + Passkey). Agents CANNOT self-register — this is the accountability root ("every agent has an accountable human behind it"). After the human gets the key, set it as the agent\'s bearer token.',
25
+ how_to_get_invite: 'Pre-launch is invite-gated for Sybil resistance. Request one by leaving your email at https://webaz.xyz/#welcome (or email contact@webaz.xyz). You can browse + read everything WITHOUT an invite — it is only needed to register and write.',
22
26
  then: 'declare your write scope at POST /api/me/agents/declarations (scope tokens from the capability matrix §②); a Passkey-bound human is exempt from scope declaration. See onboarding (③).',
23
27
  tiers: 'anonymous_read → authenticated_write (api_key→passport) → value_participant (collateral). See liability_tiers below.',
24
28
  },
@@ -20959,7 +20959,7 @@ window.handleArbitrate = async (disputeId) => {
20959
20959
  const amtEl = document.querySelector(`.arb-liable-amount[data-party="${partyId}"]`)
20960
20960
  const amt = parseFloat(amtEl?.value || '0')
20961
20961
  if (!amt || amt <= 0) {
20962
- msgEl.innerHTML = alert$('error', `${t('请填写 ')}${chk.dataset.name}${t(' 的赔付金额')}`); return
20962
+ msgEl.innerHTML = alert$('error', `${t('请填写 ')}${escHtml(chk.dataset.name)}${t(' 的赔付金额')}`); return
20963
20963
  }
20964
20964
  const entry = { user_id: partyId, role: chk.dataset.role, amount: amt }
20965
20965
  const insuranceChk = document.querySelector(`.arb-liable-insurance[data-party="${partyId}"]`)
@@ -22122,8 +22122,9 @@ async function renderSeller(app) {
22122
22122
 
22123
22123
  const skillsSection = sellerSubTab === 'skills' ? `
22124
22124
  <div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('Skill 自动化 · 让 Agent 替你接单/报价/发货')}</div>
22125
- <div id="skill-mgmt-content">${loading$()}</div>
22126
22125
  ` : ''
22126
+ // 注:Skill 真实内容由下方 #my-skills-list 同步渲染(mySkills 已同步就绪);
22127
+ // 此前这里有个 #skill-mgmt-content loading$() 占位但全文件无人填充 → spinner 永转,已删除。
22127
22128
 
22128
22129
  const productsSection = sellerSubTab === 'products' ? `
22129
22130
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
@@ -25200,7 +25201,7 @@ window.doWithdraw = async () => {
25200
25201
  msgEl.innerHTML = ''
25201
25202
  return
25202
25203
  }
25203
- msgEl.innerHTML = alert$('success', `✅ ${res.message}`)
25204
+ msgEl.innerHTML = alert$('success', `✅ ${escHtml(res.message)}`)
25204
25205
  setTimeout(() => renderWallet(document.getElementById('app')), 2000)
25205
25206
  }
25206
25207
 
@@ -26636,7 +26637,7 @@ async function renderSkillPublish(app) {
26636
26637
  <textarea class="form-control" id="skm-preview" rows="3" maxlength="500" placeholder="${t('展示部分内容吸引购买,可留空')}" style="font-size:13px;resize:vertical;margin-bottom:12px"></textarea>
26637
26638
 
26638
26639
  <div style="font-size:12px;color:#374151;font-weight:600;margin-bottom:6px">${t('技能正文(购买后解锁)')}</div>
26639
- <textarea class="form-control" id="skm-content" rows="8" maxlength="20000" placeholder="${t('完整的模板 / 提示词 / 指南 / 清单内容…')}" style="font-size:13px;resize:vertical"></textarea>
26640
+ <textarea class="form-control" id="skm-body" rows="8" maxlength="20000" placeholder="${t('完整的模板 / 提示词 / 指南 / 清单内容…')}" style="font-size:13px;resize:vertical"></textarea>
26640
26641
  </div></div>
26641
26642
  <div id="skm-publish-msg" style="margin-bottom:8px"></div>
26642
26643
  <button class="btn btn-primary" style="width:100%;font-size:14px;padding:10px" onclick="doSkmPublish()">${t('提交审核')}</button>
@@ -26655,7 +26656,7 @@ window.doSkmPublish = async () => {
26655
26656
  const price = parseFloat(document.getElementById('skm-price')?.value) || 0
26656
26657
  const summary = document.getElementById('skm-summary')?.value?.trim()
26657
26658
  const preview = document.getElementById('skm-preview')?.value?.trim()
26658
- const content = document.getElementById('skm-content')?.value
26659
+ const content = document.getElementById('skm-body')?.value
26659
26660
  const msg = document.getElementById('skm-publish-msg')
26660
26661
  if (!title || title.length < 2) { msg.innerHTML = alert$('error', t('请填写标题')); return }
26661
26662
  if (!content || !content.trim()) { msg.innerHTML = alert$('error', t('请填写技能正文内容')); return }
@@ -27554,7 +27555,7 @@ async function renderListingFollow(app, listingId) {
27554
27555
  if (!state.user) { location.hash = '#login'; return }
27555
27556
  if (state.user.role !== 'seller') { app.innerHTML = `<div style="padding:24px">${alert$('warn', t('仅卖家可跟卖'))}</div>`; return }
27556
27557
  const r = await GET('/listings/' + encodeURIComponent(listingId))
27557
- if (r?.error) { app.innerHTML = `<div style="padding:24px">${alert$('error', r.error)}</div>`; return }
27558
+ if (r?.error) { app.innerHTML = `<div style="padding:24px">${alert$('error', escHtml(r.error))}</div>`; return }
27558
27559
  const l = r.listing
27559
27560
 
27560
27561
  app.innerHTML = `
@@ -33287,7 +33288,7 @@ window.submitNote = async (orderId, parentId, draftKey) => {
33287
33288
  body: buf,
33288
33289
  })
33289
33290
  const j = await upResp.json()
33290
- if (!upResp.ok || j.error) { msg.innerHTML = alert$('error', t('图片上传失败') + `:${j.error}`); btn.disabled = false; return }
33291
+ if (!upResp.ok || j.error) { msg.innerHTML = alert$('error', t('图片上传失败') + `:${escHtml(j.error)}`); btn.disabled = false; return }
33291
33292
  hashes.push(hash)
33292
33293
  }
33293
33294
  // 步骤 2: 创建笔记(带 parent_id 时为转发)
@@ -3,7 +3,20 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <title>WebAZ</title>
6
+ <title>WebAZ — Agent-native decentralized commerce protocol</title>
7
+ <!-- Crawler / agent discoverability (static, no-JS): so a fetch of this page returns real meaning, not an empty shell. -->
8
+ <meta name="description" content="WebAZ is an open, agent-native decentralized commerce protocol. Every order is escrow-protected and flows through a state machine; on timeout the protocol auto-rules fault and executes remedy. Buyers get escrow + automatic refunds + transparent seller reputation, price history and arbitration precedents; AI agents browse, order, compare and fulfill via MCP. Pre-launch (simulated test currency, no real settlement yet).">
9
+ <link rel="canonical" href="https://webaz.xyz/">
10
+ <meta property="og:type" content="website">
11
+ <meta property="og:site_name" content="WebAZ">
12
+ <meta property="og:url" content="https://webaz.xyz/">
13
+ <meta property="og:title" content="WebAZ — Agent-native decentralized commerce protocol">
14
+ <meta property="og:description" content="Escrow-protected, state-machine commerce: timeouts auto-rule fault + remedy. Buyers get escrow + auto-refund + transparent reputation; AI agents transact via MCP. Pre-launch.">
15
+ <meta property="og:image" content="https://webaz.xyz/webaz-logo.png">
16
+ <meta name="twitter:card" content="summary_large_image">
17
+ <meta name="twitter:title" content="WebAZ — Agent-native decentralized commerce protocol">
18
+ <meta name="twitter:description" content="Escrow-protected, state-machine commerce. Buyers: escrow + auto-refund + transparent reputation. Agents transact via MCP. Pre-launch.">
19
+ <meta name="twitter:image" content="https://webaz.xyz/webaz-logo.png">
7
20
  <link rel="stylesheet" href="/style.css">
8
21
  <link rel="manifest" href="/manifest.json">
9
22
  <link rel="icon" type="image/svg+xml" href="/icon.svg">
@@ -15,6 +28,19 @@
15
28
  </head>
16
29
  <body>
17
30
  <div id="app"></div>
31
+ <!-- No-JS / crawler fallback: real content for fetchers that don't run the SPA (fixes "scrape only sees an empty <title>"). -->
32
+ <noscript>
33
+ <main style="max-width:680px;margin:40px auto;padding:0 20px;font-family:system-ui,sans-serif;line-height:1.7;color:#1f2937">
34
+ <h1>WebAZ — Agent-native decentralized commerce protocol</h1>
35
+ <p>WebAZ is an open, agent-native decentralized commerce protocol. Every order is escrow-protected and flows through a state machine; if a responsible party times out, the protocol automatically rules fault and executes the remedy.</p>
36
+ <p><strong>For buyers:</strong> funds are held in escrow and released only after you confirm receipt; sellers who don't accept/ship/deliver in time trigger an automatic refund; disputes are decided with evidence + neutral arbitration; seller reputation, price history and arbitration precedents are public before you buy.</p>
37
+ <p><strong>For AI agents:</strong> browse, compare, order and track fulfillment via the Model Context Protocol (MCP). Protocol state is public at <a href="/.well-known/webaz-protocol.json">/.well-known/webaz-protocol.json</a>; the integration contract is at <a href="/.well-known/webaz-integration.json">/.well-known/webaz-integration.json</a>.</p>
38
+ <p><strong>Status:</strong> pre-launch — WAZ is a simulated test currency; no real money settles yet.</p>
39
+ <p><strong>Browse now — no account needed:</strong> <a href="/#discover">/#discover</a> · <strong>Want in?</strong> Pre-launch is invite-gated; request an invite (leave your email) at <a href="/#welcome">/#welcome</a>.</p>
40
+ <p style="color:#6b7280;font-size:14px">Source repo opens at public launch — <a href="https://github.com/seasonsagents-art/webaz">github.com/seasonsagents-art/webaz</a> may 404 until then; the full machine-readable spec is already public at <a href="/.well-known/webaz-integration.json">/.well-known/webaz-integration.json</a>.</p>
41
+ <p style="color:#6b7280;font-size:14px">This page is an interactive app and needs JavaScript for full functionality.</p>
42
+ </main>
43
+ </noscript>
18
44
  <script src="/i18n.js"></script>
19
45
  <script src="/app.js"></script>
20
46
  </body>
@@ -1,3 +1,6 @@
1
+ // RFC-014 PR6 — 拍卖 stake 锁定/释放走整数 base-units + 绝对值落库。
2
+ import { toUnits } from '../../money.js';
3
+ import { applyWalletDelta } from '../../ledger.js';
1
4
  // ─── 拍卖常量(域内)──────────────────────────────────────────
2
5
  const AUC_MAX_WINDOW_MIN = 14 * 24 * 60; // 14 天上限
3
6
  const AUC_MIN_WINDOW_MIN = 5;
@@ -118,7 +121,7 @@ export function registerAuctionRoutes(app, deps) {
118
121
  seller_stake_locked, notes)
119
122
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now', '+' || ? || ' minutes'),?,?,?)
120
123
  `).run(id, user.id, body.listing_id ? String(body.listing_id) : null, productId, title, body.spec_json ? JSON.stringify(body.spec_json) : null, qty, cat, startingPrice, startingPrice, minIncrement, reservePrice, windowMin, sniperExtend, sellerStake, body.notes ? String(body.notes).slice(0, 500) : null);
121
- db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(sellerStake, sellerStake, user.id);
124
+ applyWalletDelta(db, user.id, { balance: -toUnits(sellerStake), staked: toUnits(sellerStake) });
122
125
  if (productId)
123
126
  db.prepare("UPDATE products SET status = 'auction_pending', updated_at = datetime('now') WHERE id = ?").run(productId);
124
127
  })();
@@ -346,19 +349,19 @@ export function registerAuctionRoutes(app, deps) {
346
349
  if (myPrev) {
347
350
  db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(myPrev.id);
348
351
  if (myPrev.stake_locked > 0)
349
- db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(myPrev.stake_locked, myPrev.stake_locked, user.id);
352
+ applyWalletDelta(db, user.id, { balance: toUnits(myPrev.stake_locked), staked: -toUnits(myPrev.stake_locked) });
350
353
  }
351
354
  // 释放别人的最高 active bid
352
355
  const others = db.prepare("SELECT id, buyer_id, stake_locked FROM auction_bids WHERE auction_id = ? AND status = 'active' AND buyer_id != ?").all(req.params.id, user.id);
353
356
  for (const o of others) {
354
357
  db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(o.id);
355
358
  if (o.stake_locked > 0)
356
- db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(o.stake_locked, o.stake_locked, o.buyer_id);
359
+ applyWalletDelta(db, o.buyer_id, { balance: toUnits(o.stake_locked), staked: -toUnits(o.stake_locked) });
357
360
  }
358
361
  // 插入新 bid
359
362
  db.prepare(`INSERT INTO auction_bids (id, auction_id, buyer_id, price, stake_locked) VALUES (?,?,?,?,?)`)
360
363
  .run(id, req.params.id, user.id, price, stake);
361
- db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(stake, stake, user.id);
364
+ applyWalletDelta(db, user.id, { balance: -toUnits(stake), staked: toUnits(stake) });
362
365
  // P1 #9:卖家 stake 动态补足(5% × current_price,余额不足则尽量补)
363
366
  const targetSellerStake = Math.max(1, Math.round(price * AUC_SELLER_STAKE_PCT * 100) / 100);
364
367
  const curSellerStake = Number(fresh.seller_stake_locked) || 0;
@@ -367,7 +370,7 @@ export function registerAuctionRoutes(app, deps) {
367
370
  const sWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(auc.seller_id);
368
371
  const canTopup = sWal ? Math.min(delta, Number(sWal.balance)) : 0;
369
372
  if (canTopup > 0) {
370
- db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(canTopup, canTopup, auc.seller_id);
373
+ applyWalletDelta(db, auc.seller_id, { balance: -toUnits(canTopup), staked: toUnits(canTopup) });
371
374
  db.prepare('UPDATE auctions SET seller_stake_locked = seller_stake_locked + ? WHERE id = ?').run(canTopup, req.params.id);
372
375
  sellerTopup = canTopup;
373
376
  }
@@ -420,7 +423,7 @@ export function registerAuctionRoutes(app, deps) {
420
423
  db.transaction(() => {
421
424
  db.prepare("UPDATE auctions SET status = 'cancelled', updated_at = datetime('now') WHERE id = ?").run(req.params.id);
422
425
  if (sellerStake > 0)
423
- db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(sellerStake, sellerStake, user.id);
426
+ applyWalletDelta(db, user.id, { balance: toUnits(sellerStake), staked: -toUnits(sellerStake) });
424
427
  if (auc.product_id)
425
428
  db.prepare("UPDATE products SET status = 'active', updated_at = datetime('now') WHERE id = ? AND status = 'auction_pending'").run(auc.product_id);
426
429
  })();
@@ -1,6 +1,9 @@
1
1
  import express from 'express';
2
2
  // RFC-007 stage 5:客观拒单仲裁翻案直接复用 L0 状态机/结算(纯 db 函数,无副作用)
3
3
  import { transition, settleFault, settleDeclinedNoFault } from '../../layer0-foundation/L0-2-state-machine/engine.js';
4
+ // RFC-014 PR5 — 争议后佣金/基金 clawback 走整数 base-units + 绝对值落库。
5
+ import { toUnits, toDecimal, mulRate } from '../../money.js';
6
+ import { applyWalletDelta } from '../../ledger.js';
4
7
  export function registerDisputesWriteRoutes(app, deps) {
5
8
  const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam } = deps;
6
9
  // ── RFC-007 stage 5:客观拒单【临时判责】的仲裁翻案 ─────────────────────────────
@@ -194,12 +197,11 @@ export function registerDisputesWriteRoutes(app, deps) {
194
197
  return;
195
198
  const total = Number(order.total_amount);
196
199
  const commRate = Number(order.snapshot_commission_rate ?? 0);
197
- const commPool = Math.round(total * commRate * 100) / 100;
198
- const fundBase = Math.round(total * FUND_BASE_RATE() * 100) / 100;
199
- const deduct = commPool + fundBase;
200
+ const deductU = mulRate(toUnits(total), commRate) + mulRate(toUnits(total), FUND_BASE_RATE());
201
+ const deduct = toDecimal(deductU);
200
202
  const sellerWallet = db.prepare("SELECT balance FROM wallets WHERE user_id = ?").get(order.seller_id);
201
- if (sellerWallet && sellerWallet.balance >= deduct) {
202
- db.prepare("UPDATE wallets SET balance = balance - ?, earned = earned - ? WHERE user_id = ?").run(deduct, deduct, order.seller_id);
203
+ if (sellerWallet && toUnits(sellerWallet.balance) >= deductU) {
204
+ applyWalletDelta(db, order.seller_id, { balance: -deductU, earned: -deductU });
203
205
  const { redirected: disputeRedirected } = settleCommission(dispute.order_id);
204
206
  depositToFund(dispute.order_id, disputeRedirected);
205
207
  const productRow = db.prepare("SELECT category_id FROM products WHERE id = ?").get(order.product_id);
@@ -248,13 +250,12 @@ export function registerDisputesWriteRoutes(app, deps) {
248
250
  if (effectiveBase <= 0)
249
251
  return;
250
252
  const commRate = Number(order.snapshot_commission_rate ?? 0);
251
- const commPool = Math.round(effectiveBase * commRate * 100) / 100;
252
- const fundBase = Math.round(effectiveBase * FUND_BASE_RATE() * 100) / 100;
253
- const deduct = Math.round((commPool + fundBase) * 100) / 100;
253
+ const deductU = mulRate(toUnits(effectiveBase), commRate) + mulRate(toUnits(effectiveBase), FUND_BASE_RATE());
254
+ const deduct = toDecimal(deductU);
254
255
  const sellerWallet = db.prepare("SELECT balance FROM wallets WHERE user_id = ?").get(order.seller_id);
255
- if (sellerWallet && sellerWallet.balance >= deduct) {
256
- if (deduct > 0) {
257
- db.prepare("UPDATE wallets SET balance = balance - ?, earned = earned - ? WHERE user_id = ?").run(deduct, deduct, order.seller_id);
256
+ if (sellerWallet && toUnits(sellerWallet.balance) >= deductU) {
257
+ if (deductU > 0) {
258
+ applyWalletDelta(db, order.seller_id, { balance: -deductU, earned: -deductU });
258
259
  }
259
260
  const { redirected } = settleCommission(dispute.order_id, effectiveBase);
260
261
  depositToFund(dispute.order_id, redirected, effectiveBase);
@@ -1,4 +1,7 @@
1
1
  import { buildCartMandate, buildPaymentMandate, signMandate } from './ap2-mandate.js';
2
+ // RFC-014 PR3 — 金额走整数 base-units;钱包写绝对值(防 REAL 浮点加法 dust)。
3
+ import { toUnits, toDecimal, mulQty, mulRate } from '../../money.js';
4
+ import { applyWalletDelta } from '../../ledger.js';
2
5
  export function registerOrdersCreateRoutes(app, deps) {
3
6
  const { db, auth, isTrustedRole, generateId, generateRecipientCode, DONATION_VALID_PCTS, INTERNAL_AUDITOR_ID, addHours, getActiveFlashSale, applyCouponToOrder, getProtocolParam, getProductShareChain, isAllowedSponsor, checkStockAndMaybeDelist, auditSponsorChainCross, appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep, broadcastSystemEvent, signPassport, issuerAddress } = deps;
4
7
  app.post('/api/orders', async (req, res) => {
@@ -170,18 +173,25 @@ export function registerOrdersCreateRoutes(app, deps) {
170
173
  db.prepare(`UPDATE price_sessions SET used_at = datetime('now') WHERE token = ?`).run(session_token);
171
174
  }
172
175
  const basePrice = product.price;
173
- // 多件:subtotal = basePrice * qty,减 coupon,再加保险(按 subtotal 计费)
174
- const subtotal = Math.round(basePrice * reqQty * 100) / 100;
175
- const priceAfterCoupon = Math.max(0, Math.round((subtotal - couponDiscount) * 100) / 100);
176
+ // RFC-014:全部金额在整数 base-units 上算(精确),再 toDecimal 落库/响应。
177
+ // 多件:subtotal = basePrice × qty,减 coupon,再加保险(按 subtotal 计费)
178
+ const basePriceU = toUnits(basePrice);
179
+ const subtotalU = mulQty(basePriceU, reqQty);
180
+ const priceAfterCouponU = Math.max(0, subtotalU - toUnits(couponDiscount));
176
181
  const insuranceRate = getProtocolParam('order_insurance_rate', 0.01);
177
- const insurancePremium = buy_insurance ? Math.round(priceAfterCoupon * insuranceRate * 100) / 100 : 0;
178
- const totalAmount = Math.max(0, Math.round((priceAfterCoupon + insurancePremium) * 100) / 100);
182
+ const insurancePremiumU = buy_insurance ? mulRate(priceAfterCouponU, insuranceRate) : 0;
183
+ const totalAmountU = Math.max(0, priceAfterCouponU + insurancePremiumU);
179
184
  // B5 主动捐赠 — 按订单总额 × 比例算(额外扣款,进 charity_fund)
180
- const donationAmount = donationPctNum > 0 ? Math.round(totalAmount * donationPctNum * 100) / 100 : 0;
185
+ const donationAmountU = donationPctNum > 0 ? mulRate(totalAmountU, donationPctNum) : 0;
186
+ // decimal 视图(落库 / 下游 AP2 / 响应,均为 base-unit 干净值)
187
+ const subtotal = toDecimal(subtotalU);
188
+ const insurancePremium = toDecimal(insurancePremiumU);
189
+ const totalAmount = toDecimal(totalAmountU);
190
+ const donationAmount = toDecimal(donationAmountU);
181
191
  const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
182
192
  if (!wallet)
183
193
  return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
184
- if (wallet.balance < totalAmount + donationAmount)
194
+ if (toUnits(wallet.balance) < totalAmountU + donationAmountU)
185
195
  return void res.json({ error: `余额不足:需 ${(totalAmount + donationAmount).toFixed(2)} WAZ(含 ${donationAmount} WAZ 捐赠),当前 ${wallet.balance} WAZ` });
186
196
  const now = new Date();
187
197
  const orderId = generateId('ord');
@@ -259,13 +269,14 @@ export function registerOrdersCreateRoutes(app, deps) {
259
269
  catch (e) {
260
270
  console.warn('[order-chain] genesis event failed:', e.message);
261
271
  }
262
- db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?')
263
- .run(totalAmount, totalAmount, user.id);
272
+ // RFC-014:钱包托管锁定走绝对值落库(整数 base-units)
273
+ applyWalletDelta(db, user.id, { balance: -totalAmountU, escrowed: totalAmountU });
264
274
  // B5:捐赠 — 从 balance 扣 + 进 charity_fund + 记一笔 donation txn(事务内原子)
265
- if (donationAmount > 0) {
266
- db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(donationAmount, user.id);
267
- db.prepare(`UPDATE charity_fund SET balance = balance + ?, total_donated = total_donated + ?, updated_at = datetime('now') WHERE id = 'main'`)
268
- .run(donationAmount, donationAmount);
275
+ if (donationAmountU > 0) {
276
+ applyWalletDelta(db, user.id, { balance: -donationAmountU });
277
+ const cf = db.prepare("SELECT COALESCE(balance,0) balance, COALESCE(total_donated,0) total_donated FROM charity_fund WHERE id = 'main'").get();
278
+ db.prepare(`UPDATE charity_fund SET balance = ?, total_donated = ?, updated_at = datetime('now') WHERE id = 'main'`)
279
+ .run(toDecimal(toUnits(cf?.balance ?? 0) + donationAmountU), toDecimal(toUnits(cf?.total_donated ?? 0) + donationAmountU));
269
280
  db.prepare(`INSERT INTO charity_fund_txns (id, kind, from_user_id, to_user_id, amount, related_order_id, note)
270
281
  VALUES (?, 'donation', ?, NULL, ?, ?, ?)`).run(generateId('cft'), user.id, donationAmount, orderId, `下单时捐赠 ${(donationPctNum * 100).toFixed(1)}%`);
271
282
  // 同步 charity_reputation(捐款荣誉)
@@ -10,6 +10,7 @@ import { buildIntegrationContract } from '../integration-contract.js';
10
10
  import { buildVerifiabilityIndex } from '../verifiability-index.js';
11
11
  import { buildEconomicParticipation } from '../economic-participation.js';
12
12
  import { buildNegativeSpace } from '../negative-space.js';
13
+ import { buildAcpProductFeed } from '../acp-feed.js';
13
14
  export function registerPublicUtilsRoutes(app, deps) {
14
15
  const { db, MASTER_SEED, NODE_ENV, SERVICE_START_MS, rateLimitOk, generateManifest, getUser, logError, issuerAddress } = deps;
15
16
  app.get('/api/health', (_req, res) => {
@@ -90,6 +91,21 @@ export function registerPublicUtilsRoutes(app, deps) {
90
91
  // software_version = 本代码 npm/release semver(= package.json,自动同步,永不漂移)
91
92
  schema_version: CONTRACT_VERSION,
92
93
  software_version: SOFTWARE_VERSION,
94
+ // 给【终端用户/买家】的一句话价值主张 —— 陌生 agent / 爬虫抓 manifest 第一眼就懂"对买家有什么用",
95
+ // 不只是抽象 tagline + 技术 description。与 MCP webaz_info.for_end_user 对齐(两个发现面一致)。
96
+ for_end_user: {
97
+ one_liner: 'Shop with built-in protection: every order is escrow-held and only released after you confirm receipt; if the seller is late, you are auto-refunded. / 自带保护的购物:每笔订单托管,确认收货才放款;卖家超时自动退款给你。',
98
+ why_use: [
99
+ 'Escrow on every order — money released only after you confirm receipt (or auto-confirm window).',
100
+ 'Automatic fault ruling — seller fails to accept/ship/deliver in time → auto-refund, no haggling.',
101
+ 'Disputes with evidence + neutral arbitration.',
102
+ 'Decision-ready transparency — seller reputation, price history and arbitration precedents are public before you buy.',
103
+ 'Agent-native — your AI agent can compare, order and track fulfillment via MCP.',
104
+ ],
105
+ honesty: 'Pre-launch: WAZ is a simulated test currency; no real money settles yet.',
106
+ try_it: 'Browse now, no account needed → https://webaz.xyz/#discover',
107
+ get_access: 'Want to register? Pre-launch is invite-gated — request an invite at https://webaz.xyz/#welcome (browsing needs no invite).',
108
+ },
93
109
  network_state: {
94
110
  phase,
95
111
  real_users_on_canonical: realUsers,
@@ -117,6 +133,8 @@ export function registerPublicUtilsRoutes(app, deps) {
117
133
  },
118
134
  // 公开披露文档(#1050) — 协议层"钱怎么流"的源真理(协议外可读)
119
135
  disclosures: {
136
+ // 源码仓库 launch 前私有,下列 github 链接可能 404(launch 时开),不是死项目;机器可读 spec 已全在 /.well-known/*。
137
+ source_status: 'repo private until W8 public launch — github.com links below may 404 until then (they open at launch); the full spec is already public via /.well-known/*.',
120
138
  economic_model: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ECONOMIC-MODEL.md',
121
139
  mlm_compliance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/MLM-COMPLIANCE.md',
122
140
  agent_governance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/AGENT-GOVERNANCE.md',
@@ -138,6 +156,7 @@ export function registerPublicUtilsRoutes(app, deps) {
138
156
  event_stream: 'https://webaz.xyz/api/agent/events?since=<cursor>', // ⑥ 事件游标流(party-gated,需 auth)
139
157
  passport: 'https://webaz.xyz/api/me/agents/:apiKeyPrefix/passport', // ⑤ 可验护照
140
158
  did: 'https://webaz.xyz/.well-known/did.json',
159
+ acp_product_feed: 'https://webaz.xyz/.well-known/webaz-acp-feed.json', // RFC-015 P0 — ACP 风格商品发现 feed(只读;is_eligible_checkout=false)
141
160
  },
142
161
  // 路线图 — 回应"知道还有哪些没做"的诚实化第三层。哲学:公开当前到达点 + 已知未做项,不承诺时间表。
143
162
  roadmap: {
@@ -270,6 +289,15 @@ export function registerPublicUtilsRoutes(app, deps) {
270
289
  };
271
290
  app.get('/.well-known/webaz-negative-space.json', negativeSpace);
272
291
  app.get('/api/agent/negative-space', negativeSpace);
292
+ // RFC-015 P0 —— ACP product feed:把现有商品投影成 OpenAI Agentic Commerce 的 feed 形状,
293
+ // 让 ACP/ChatGPT agent 能【发现】WebAZ 商品(只读,无钱)。诚实门控:is_eligible_checkout 恒 false
294
+ // (ACP /complete 是卡+PSP,WebAZ 未接);currency=WAZ 是模拟单位。详见 buildAcpProductFeed + RFC-015。
295
+ const acpFeed = (_req, res) => {
296
+ res.setHeader('Cache-Control', 'public, max-age=300');
297
+ res.json(buildAcpProductFeed(db));
298
+ };
299
+ app.get('/.well-known/webaz-acp-feed.json', acpFeed);
300
+ app.get('/api/agent/acp-feed', acpFeed);
273
301
  // W3C DID Document(B.6 b DID 短期 mapping,2026-05-30):
274
302
  // did:web:webaz.xyz 通过 HTTPS 解析到这里(W3C did:web spec §3.2)
275
303
  // verificationMethod 用 EcdsaSecp256k1RecoveryMethod2020 + CAIP-10 blockchainAccountId