Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - hexuria/berth-web: Host and buyer web UI for Berth Market + Berthos. No Docker, no custody. · GitHub
Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berth-web: Host and buyer web UI for Berth Market + Berthos. No Docker, no custody. · GitHub
Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berth-web: Host and buyer web UI for Berth Market + Berthos. No Docker, no custody. · GitHub
Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - hexuria/berth-web: Host and buyer web UI for Berth Market + Berthos. No Docker, no custody. · GitHub
Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berth-web: Host and buyer web UI for Berth Market + Berthos. No Docker, no custody. · GitHub
Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - hexuria/berth-web: Host and buyer web UI for Berth Market + Berthos. No Docker, no custody. · GitHub
Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - hexuria/berth-web: Host and buyer web UI for Berth Market + Berthos. No Docker, no custody. · GitHub
Skip to content

Repository files navigation

berth-web

Public host / buyer UI for Berth Market and Berthos.

This repo is not the node and not the market. It is the human surface:

RoleWhat this UI doesWhat it does not do
HostCopy berth doctor / berth node up commands. Show eligibility if a Berthos URL is set (mocked in demo / CI). When the doctor is green for vm-guest, park that guest as kind=desktop.linux on Sepolia. After a buyer pays that SKU, show occupancy and receipt 90/10 for the parked listing.Start Docker, a hypervisor, or a guest. List laptop / host-desktop. Render a host-desktop viewer.
BuyerCatalog, unpaid invoke → HTTP 402 quote, demo pay with test:<walletId>, receipt + leaseId, link to berth view when a view URL exists.Hold keys. Settle USDC. Drive the host desktop.

Payments live in berth-market (x402, USDC, wallets, receipts, end lease). Isolation lives in berthos (doctor, loopback HTTP, labeled Linux guest). Two-role walkthrough: docs/DEMO.md.

Hard rules

  • Public GitHub. CI is free GitHub Actions: lint, unit + integration, Playwright Chromium. No paid runners. No secrets.
  • No Docker, no hypervisor, no guest runtime in this repo.
  • No custody of keys. No CDP secrets in CI.
  • Never present a host desktop or laptop as a public listing. The UI refuses kind / class of laptop and host-desktop the same way the market API does (forbidden_class).
  • No BERTH token. No own chain. No AgentMail.
  • Staging network is Base Sepolia (eip155:84532). Mainnet is off in this UI.

Quick start

npm install
npm test
npm run dev # http://127.0.0.1:5173 (demo market via MSW)

Default demo mode talks to an in-memory market (MSW). npm test and Playwright need no network and no secrets.

Live mode is VITE_MARKET_URL + the Vite proxy. Do not fetch :8787 from the page.

# other terminal: hexuria/berth-market
npm start # http://127.0.0.1:8787 (MemoryWallet + TestFacilitator)# this repoexport VITE_MARKET_URL=http://127.0.0.1:8787
# optional loopback node (same role as BERTHOS_URL on the market)export VITE_BERTHOS_URL=http://127.0.0.1:7432
npm run dev # browser uses /mkt → :8787 and /bos → :7432

The page stays on :5173. It fetches same-origin/mkt/* and /bos/*. Vite rewrites those to the env origins. A direct browser call to http://127.0.0.1:8787 fails CORS (Failed to fetch).

Browser pathUpstream (env)
/mktVITE_MARKET_URL (e.g. http://127.0.0.1:8787)
/bosVITE_BERTHOS_URL (e.g. http://127.0.0.1:7432)

Default npm start on the market is MemoryWallet (no WALLET_ADAPTER=cdp). This UI then POST /wallets/agent + POST /wallets/:id/fund and enables Pay with test signature (test:<walletId>) so unpaid invoke → 402 → 200 works locally. If /health reports walletAdapter=cdp or a live facilitatorUrl, test-signature pay stays disabled and the page says so. The live-mode banner shows walletAdapter and facilitator from /health (never facilitatorUrl or other secrets).

Leave both env vars unset for demo MSW (CI default). Restart npm run dev after changing env. Do not set CDP keys, wallet secrets, or NETWORK=eip155:8453 here.

Guest view (GET /v1/leases/{id}/view / berth view) needs the labeled berthos-linux-desktop:v1 image on the node host. Rebuild it after pulling berthos — an old image can lease but omit a viewer:

# in hexuria/berthos, on the node host — not this repo
docker build -t berthos-linux-desktop:v1 images/linux-desktop

Scripts

ScriptPurpose
npm run lintESLint + tsc --noEmit
npm testVitest unit + MSW integration
npm run test:e2ePlaywright Chromium: demo MSW + Vite /mkt + /bos against an in-process mock (HTTP + host-park desktop.linux → buyer pay → host occupancy/90-10 + cdp_split_90_10 honesty copy on buyer/host + CDP health disable + host eligibility, no Docker)
npm run buildProduction bundle

Talks to

berth-market over HTTP (README):

MethodPathUsed for
GET/listingsCatalog
POST/listingsHost park of eligible desktop.linux; refuse laptop / host-desktop (forbidden_class)
GET/listings/:id/invokeUnpaid → 402 + PAYMENT-REQUIRED; paid → receipt
POST/wallets/treasurySeller / parent treasury (live MemoryWallet)
POST/wallets/agentCapped child; live MemoryWallet bootstrap
POST/wallets/:id/fundTest USDC for test:<walletId>
GET/wallets/:idDemo seed wallet, or the live test agent
GET/receiptsHost occupancy / earn (?listingId= for the parked guest)
GET/receipts/:idReceipt (onChainSettlement when the market set it)
POST/receipts/:id/endEnd Berthos lease; occupancy seconds, not a second charge
GET/healthLiveness + identity (walletAdapter / facilitator; facilitatorUrl is not shown in the UI)

Demo and local MemoryWallet pay encode a v2 PAYMENT-SIGNATURE whose inner signature is test:<walletId> — the market's TestFacilitator format. Receipt 90/10 is accounting (sellerAtomic / protocolAtomic). When onChainSettlement is payTo_100, on-chain USDC went 100% to payTo — this UI does not call that a Base split. cdp_split_90_10 is the only on-chain 90/10.

New listings (demo seed + the buyer New listing helper) default to Base Sepolia (eip155:84532) / Sepolia USDC. A stored eip155:8453 row is shown as-is and is not rewritten.

berthos over loopback HTTP, optional (README):

MethodPathUsed for
GET/v1/eligibilityHost eligibility (ok, class, checks[])
GET/v1/leases/{id}/view{ viewer_url } for the guest, not the host desktop

CI mocks both. A missing node is shown as unreachable; this UI does not start it.

License

MIT.

About

Host and buyer web UI for Berth Market + Berthos. No Docker, no custody.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages