diff --git a/docs/explanation/access-control-model.md b/docs/explanation/access-control-model.md
index dc118e5..26cf6d4 100644
--- a/docs/explanation/access-control-model.md
+++ b/docs/explanation/access-control-model.md
@@ -68,8 +68,9 @@ level, it can access the track without paying.
In Classic mode, a listener pays a fixed amount of the configured chain's
native token to unlock full playback. On the current Product DevNet/Paseo Asset
-Hub runtime rail, that token is PAS. The payment goes directly and immediately
-to the artist's wallet - no intermediary, no payout schedule, no platform cut.
+Hub runtime rail, that token is PAS; a DOT-backed Polkadot Hub EVM chain would
+display DOT instead. The payment goes directly and immediately to the artist's
+wallet - no intermediary, no payout schedule, no platform cut.
Once a listener has paid for a track, their wallet is recorded on-chain. They
can return and play the track at any time without paying again, even if the
diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md
index 5e0f82d..df6ecc4 100644
--- a/docs/explanation/product-devnet-architecture.md
+++ b/docs/explanation/product-devnet-architecture.md
@@ -196,7 +196,8 @@ Adapters:
Classic unlock no longer passes a loose `(runtimeAddress, contentHash, value)`
triple into runtime writers. The catalog hook creates a typed native runtime
payment intent first: asset symbol derived from the configured EVM chain
-(`PAS` on the current Product DevNet/Paseo Asset Hub chain), rail
+(`PAS` on the current Product DevNet/Paseo Asset Hub chain, `DOT` on a
+DOT-backed Polkadot Hub EVM chain), rail
`runtime-native`, runtime address, content hash, and 18-decimal native amount.
The amount comes from the runtime or catalog API `pricePlanck` value when
available; `priceDot` is only the rounded display string. The viem and Product
diff --git a/web/e2e/artist-publish.spec.ts b/web/e2e/artist-publish.spec.ts
index c3fb558..f06d3f1 100644
--- a/web/e2e/artist-publish.spec.ts
+++ b/web/e2e/artist-publish.spec.ts
@@ -25,6 +25,8 @@ declare global {
const fixtureDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures');
const audioFixture = path.join(fixtureDir, 'artist-release.wav');
const coverFixture = path.join(fixtureDir, 'artist-cover.svg');
+const E2E_NATIVE_PAYMENT_SYMBOL = 'PAS';
+const E2E_ARTIST_SHARE_PERCENT = '72.5';
async function readArtistPublishState(page: Page) {
return page.evaluate(() => window.__DOTIFY_E2E_ARTIST_PUBLISH__ as ArtistPublishE2eState | undefined);
@@ -76,12 +78,12 @@ async function completeReleaseDraft(page: Page) {
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByTestId('release-access-select').selectOption('classic');
await page.getByTestId('release-price-input').fill('0.75');
- await page.getByTestId('release-royalty-input').fill('7250');
+ await page.getByTestId('release-royalty-input').fill(E2E_ARTIST_SHARE_PERCENT);
await page.getByRole('button', { name: 'Continue' }).click();
const reviewPanel = page.locator('.release-review');
await expect(reviewPanel.getByText('E2E Published Signal')).toBeVisible();
- await expect(reviewPanel.getByText('0.75 DOT')).toBeVisible();
+ await expect(reviewPanel.getByText(`0.75 ${E2E_NATIVE_PAYMENT_SYMBOL}`)).toBeVisible();
}
test('artist can create a runtime, publish a release, and see it in the listener catalog', async ({ page }) => {
@@ -102,7 +104,7 @@ test('artist can create a runtime, publish a release, and see it in the listener
const publishedCard = page.getByTestId('track-card').filter({ hasText: 'E2E Published Signal' });
await expect(publishedCard).toContainText('E2E Artist');
await expect(publishedCard).toContainText('A deterministic artist publish e2e release.');
- await expect(publishedCard).toContainText('0.75 DOT');
+ await expect(publishedCard).toContainText(`0.75 ${E2E_NATIVE_PAYMENT_SYMBOL}`);
});
test('artist onboarding handles a missing wallet without enabling profile creation', async ({ page }) => {
diff --git a/web/e2e/classic-unlock.spec.ts b/web/e2e/classic-unlock.spec.ts
index f135e91..6466edb 100644
--- a/web/e2e/classic-unlock.spec.ts
+++ b/web/e2e/classic-unlock.spec.ts
@@ -16,18 +16,20 @@ async function readClassicUnlockState(page: Page) {
return page.evaluate(() => window.__DOTIFY_E2E_CLASSIC_UNLOCK__ as ClassicUnlockE2eState | undefined);
}
+const E2E_NATIVE_PAYMENT_SYMBOL = 'PAS';
+
test('Classic track stays locked before payment and unlocks full playback after payment', async ({ page }) => {
await page.goto('/');
const trackCard = page.getByTestId('track-card');
await expect(trackCard).toContainText('Deterministic Classic Unlock');
- await expect(trackCard).toContainText('0.5 DOT');
+ await expect(trackCard).toContainText(`0.5 ${E2E_NATIVE_PAYMENT_SYMBOL}`);
await page.getByTestId('track-card-open').click();
await expect(page.getByTestId('locked-player-state')).toContainText('Listening closed');
await expect(page.getByTestId('access-warning')).toContainText('Support and open this track');
- await expect(page.getByTestId('access-warning')).toContainText('0.5 DOT');
+ await expect(page.getByTestId('access-warning')).toContainText(`0.5 ${E2E_NATIVE_PAYMENT_SYMBOL}`);
const beforePayment = await readClassicUnlockState(page);
expect(beforePayment?.fullKeyRequests ?? 0).toBe(0);
diff --git a/web/src/components/AccessGateOverlay.tsx b/web/src/components/AccessGateOverlay.tsx
index 724999d..7d5113c 100644
--- a/web/src/components/AccessGateOverlay.tsx
+++ b/web/src/components/AccessGateOverlay.tsx
@@ -1,20 +1,24 @@
import { LockKeyhole } from 'lucide-react';
import { Dialog } from './Dialog';
import type { AccessGate } from '../shared/types';
+import { nativeRuntimeAmountLabel, type DotifyNativeRuntimeAsset } from '../features/payments/paymentModel';
export function AccessGateOverlay({
gate,
+ nativePaymentAsset,
onDismiss,
onPay,
onSignIn
}: {
gate: AccessGate;
+ nativePaymentAsset: Pick;
onDismiss: () => void;
onPay?: () => void;
onSignIn?: () => void;
}) {
const configuredSplitBps = gate.track.royaltySplits.reduce((total, split) => total + split.bps, 0);
const artistRemainderBps = Math.max(0, 10_000 - configuredSplitBps);
+ const supportAmount = nativeRuntimeAmountLabel(gate.track.priceDot, nativePaymentAsset);
return (
You set the access
- Free for verified humans, or a price in DOT - per track, your call.
+ Free for verified humans, or a price in {nativePaymentSymbol} - per track, your call.
diff --git a/web/src/views/artist/ReleasesTab.tsx b/web/src/views/artist/ReleasesTab.tsx
index 8db6240..949d8da 100644
--- a/web/src/views/artist/ReleasesTab.tsx
+++ b/web/src/views/artist/ReleasesTab.tsx
@@ -17,6 +17,7 @@ type ReleasesTabProps = {
onUpdateReleaseAccessMode: (track: CatalogTrack, accessMode: AccessMode, priceDot: string, personhoodLevel: PersonhoodLevel) => void;
onSetReleaseActive: (track: CatalogTrack, active: boolean) => void;
releaseActionId: string | null;
+ nativePaymentSymbol: string;
/** Into orbit (Constellation phase C): id of a release that just landed on
* chain while the console was open; its card plays a one-shot arrival. */
arrivedReleaseId?: string | null;
@@ -34,6 +35,7 @@ export function ReleasesTab({
onUpdateReleaseAccessMode,
onSetReleaseActive,
releaseActionId,
+ nativePaymentSymbol,
arrivedReleaseId = null
}: ReleasesTabProps) {
const selectedRelease = artistTracks.find(track => track.id === selectedReleaseId) ?? artistTracks[0] ?? null;
@@ -103,7 +105,7 @@ export function ReleasesTab({
{track.active === false
? 'Inactive'
: track.accessMode === 'classic'
- ? `${track.priceDot} DOT`
+ ? `${track.priceDot} ${nativePaymentSymbol}`
: track.accessMode === 'free'
? 'Free'
: track.personhoodLevel}
@@ -137,7 +139,7 @@ export function ReleasesTab({
{accessModeLabel(selectedRelease)}
{selectedRelease.accessMode === 'classic'
- ? `${selectedRelease.priceDot} DOT`
+ ? `${selectedRelease.priceDot} ${nativePaymentSymbol}`
: selectedRelease.accessMode === 'free'
? 'Free'
: 'Free for verified humans'}
@@ -194,7 +196,7 @@ export function ReleasesTab({