diff --git a/README.md b/README.md
index e8fe243..b125d37 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Sysnode is designed to make that information easier to read, easier to verify, a
- Sentry Node trend data over selectable time ranges.
- Current governance proposals, budgets, support, vote counts, estimated payout-selection start, and next superblock.
- Country-level Sentry Node distribution.
-- SYS market context including price, volume, market cap, supply, and exchange links.
+- SYS market context including price, volume, market cap, and supply.
- Educational pages covering what Sentry Nodes are and how to approach setup.
## Tech Stack
diff --git a/src/components/ExchangeGrid.js b/src/components/ExchangeGrid.js
index 7680677..d552a57 100644
--- a/src/components/ExchangeGrid.js
+++ b/src/components/ExchangeGrid.js
@@ -1,6 +1,8 @@
import React from 'react';
export default function ExchangeGrid(props) {
+ if (!Array.isArray(props.exchanges) || props.exchanges.length === 0) return null;
+
return (
diff --git a/src/components/ExchangeGrid.test.js b/src/components/ExchangeGrid.test.js
new file mode 100644
index 0000000..414a662
--- /dev/null
+++ b/src/components/ExchangeGrid.test.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import ExchangeGrid from './ExchangeGrid';
+import SYS_EXCHANGES from '../data/exchanges';
+
+test.each([[], undefined, null])('omits the exchange section and its market CTA without listings (%p)', (exchanges) => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ expect(screen.queryByText(/Get your Syscoin|View all markets/)).not.toBeInTheDocument();
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
+});
+
+test('current exchange data contains no unverified or planned listings', () => {
+ expect(SYS_EXCHANGES).toEqual([]);
+});
+
+test('can still render an explicitly supplied listing', () => {
+ render();
+ const listing = screen.getByRole('link', { name: /Example exchange/ });
+ expect(listing).toHaveAttribute('href', 'https://exchange.example/trade/SYS-USDT');
+ expect(listing).toHaveAttribute('rel', 'noopener noreferrer');
+ expect(screen.getByAltText('Example exchange logo')).toBeInTheDocument();
+});
diff --git a/src/data/exchanges.js b/src/data/exchanges.js
index a3c70d6..f65fc9a 100644
--- a/src/data/exchanges.js
+++ b/src/data/exchanges.js
@@ -1,34 +1,5 @@
-const SYS_EXCHANGES = [
- {
- name: 'Gate',
- logo: 'https://coin-images.coingecko.com/markets/images/60/small/Frame_1.png?1747795534',
- href: 'https://www.gate.com/trade/SYS_USDT',
- },
- {
- name: 'HTX',
- logo: 'https://coin-images.coingecko.com/markets/images/25/small/htx.png?1721712842',
- href: 'https://www.htx.com/trade/sys_usdt',
- },
- {
- name: 'MEXC',
- logo: 'https://coin-images.coingecko.com/markets/images/409/small/164286be-32a5-4b58-978c-d072eea00eb9.jpeg?1775619316',
- href: 'https://www.mexc.com/exchange/SYS_USDT',
- },
- {
- name: 'Bitget',
- logo: 'https://coin-images.coingecko.com/markets/images/540/small/2023-07-25_21.47.43.jpg?1706864507',
- href: 'https://www.bitget.com/spot/SYSUSDT',
- },
- {
- name: 'CoinEx',
- logo: 'https://coin-images.coingecko.com/markets/images/135/small/coinex.jpg?1706864305',
- href: 'https://www.coinex.com/en/exchange/sys-usdt',
- },
- {
- name: 'KuCoin',
- logo: 'https://coin-images.coingecko.com/markets/images/61/small/kucoin.png?1706864282',
- href: 'https://www.kucoin.com/trade/SYS-USDT',
- },
-];
+// No trading venue is currently verified for display. Keep this empty until
+// a current listing is confirmed; planned venues must not appear as live.
+const SYS_EXCHANGES = [];
export default SYS_EXCHANGES;
diff --git a/src/pages/NetworkAvailability.test.js b/src/pages/NetworkAvailability.test.js
index 1b7e81a..6181854 100644
--- a/src/pages/NetworkAvailability.test.js
+++ b/src/pages/NetworkAvailability.test.js
@@ -78,3 +78,15 @@ test('technical version and protocol identifiers keep their ungrouped representa
expect(screen.queryByText('320,000')).not.toBeInTheDocument();
expect(screen.queryByText('70,016')).not.toBeInTheDocument();
});
+
+test('Home keeps market data and navigation without an empty exchange promotion', async () => {
+ fetchNetworkStats.mockResolvedValue(validNetworkStats());
+ render();
+ await screen.findByText('800,000,000 SYS');
+ expect(screen.getByText('Syscoin Market')).toBeInTheDocument();
+ expect(screen.getByText('SYS Price')).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: 'View Proposals' })).toHaveAttribute('href', '/governance');
+ expect(screen.queryByText(/Get your Syscoin|View all markets/)).not.toBeInTheDocument();
+ expect(document.querySelector('.market-exchanges')).toBeNull();
+ expect(document.querySelector('.panel--market')).toBeInTheDocument();
+});
diff --git a/tests/browser/network-availability.spec.cjs b/tests/browser/network-availability.spec.cjs
index 51db1c6..30b3642 100644
--- a/tests/browser/network-availability.spec.cjs
+++ b/tests/browser/network-availability.spec.cjs
@@ -105,3 +105,38 @@ test('governance stats outage preserves proposals without false support at phone
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(321);
await page.screenshot({ path: testInfo.outputPath('governance-stats-unavailable.png'), fullPage: true });
});
+
+for (const width of [320, 390]) {
+ test(`Home keeps market data without an empty exchange section at ${width}px`, async ({ page }, testInfo) => {
+ const now = Date.now();
+ await page.setViewportSize({ width, height: 640 });
+ await page.route('**/auth/me', request => request.fulfill({ status: 401, json: { error: 'unauthorized' } }));
+ await page.route('**/mnstats', request => request.fulfill({ json: {
+ stats_meta: { groups: Object.fromEntries(['chain', 'node', 'governance', 'market'].map(name =>
+ [name, { status: 'ready', updated_at: now }])) },
+ stats: {
+ mn_stats: { total: '3,123', enabled: '2,139', pose_banned: '984', total_locked: '312,300,000', current_supply: '800,000,000',
+ coins_percent_locked: '39.04%', roi: '5.20%', roi_one: '7.02%', roi_two: '10.40%' },
+ blockchain_stats: { version: 50102, connections: '2,329,762' },
+ superblock_stats: { superblock_date: 'October 30th 2026, 1:00:00 pm', superblock_next_epoch_sec: Math.floor(now / 1000) + 86400 },
+ price_stats: { price_usd: '0.0019', price_change: '4.04', volume_usd: '56150', market_cap_usd: '1749453', circulating_supply: '800000000' },
+ }, mapData: {},
+ } }));
+ await page.route('**/mncount', request => request.fulfill({ json: [{ date: '2026-10-21', users: 3123 }] }));
+ await page.goto('/');
+ await expect(page.getByText('800,000,000 SYS', { exact: true })).toBeVisible();
+ await expect(page.locator('.chart-canvas canvas')).toHaveCount(1);
+ await expect(page.locator('.metric-card').filter({ hasText: 'Enabled Sentry Nodes' }).getByText('2,139', { exact: true })).toBeVisible();
+ await expect(page.locator('.panel--market')).toContainText('SYS Price');
+ await expect(page.locator('.panel--market')).toContainText('312,300,000 SYS');
+ await expect(page.locator('.market-exchanges')).toHaveCount(0);
+ await expect(page.getByRole('link', { name: 'View all markets' })).toHaveCount(0);
+ await expect(page.locator('a[href*="mexc"], a[href*="kucoin"], a[href*="coinex"], a[href*="bitget"], a[href*="htx"], a[href*="gate.com"]')).toHaveCount(0);
+ await expect(page.getByRole('link', { name: 'View Proposals', exact: true })).toHaveAttribute('href', '/governance');
+ expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width + 1);
+ await page.screenshot({ path: testInfo.outputPath(`home-no-exchanges-${width}.png`), fullPage: true });
+ await page.setViewportSize({ width, height: 1000 });
+ await page.locator('.panel--market').evaluate(element => window.scrollTo(0, window.scrollY + element.getBoundingClientRect().top - 100));
+ await page.locator('.panel--market').screenshot({ path: testInfo.outputPath(`market-no-exchanges-${width}.png`) });
+ });
+}