User Personas and Journeys
Protocol Media Labs serves three distinct user types, each with unique needs, technical requirements, and security profiles. This document defines each persona and maps their complete journey through the platform.
📖 Reader Persona
Profile
- Primary Goal: Access quality journalism with minimal friction
- Technical Sophistication: Low to medium (web2-native)
- Crypto Experience: None required
- Privacy Preference: Moderate (values privacy but prioritizes convenience)
- Payment Willingness: Micropayments and subscriptions
User Journey
Technical Flow
1. Discovery Phase
// Reader lands on platform
async function initializeReaderExperience() {
// Load trending content from CDN
const catalog = await fetchCatalog({
source: 'cdn_primary',
fallback: 'walrus_aggregator'
});
// Personalize without authentication
const recommendations = await getLocalRecommendations({
browsing_history: localStorage.getItem('anon_history'),
trending_topics: catalog.trending
});
return { catalog, recommendations };
}
2. Authentication Phase
// Seamless zkLogin for web2 users
async function authenticateReader(provider: 'google' | 'apple' | 'facebook') {
// Generate ephemeral keys (24hr validity)
const ephemeralKeys = Ed25519Keypair.generate();
const maxEpoch = await getCurrentEpoch() + 24; // 24hr session
// OAuth flow with minimal data collection
const zkProof = await zkLogin.authenticate({
provider,
ephemeralPublicKey: ephemeralKeys.getPublicKey(),
maxEpoch,
nonce: generateNonce(),
selective_disclosure: ['sub'] // Only subject ID
});
// Derive Sui address (no PII stored)
const readerAddress = deriveZkLoginAddress(zkProof);
// Store session locally
await storeSession({
address: readerAddress,
ephemeralKeys,
expires: maxEpoch
});
return readerAddress;
}
3. Purchase Phase
// Gasless content purchase
async function purchaseArticle(
contentId: ObjectId,
readerAddress: Address
) {
// Check sponsorship eligibility
const eligible = await checkSponsorshipEligibility(readerAddress);
if (eligible) {
// Sponsored transaction for smooth UX
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::entitlements::purchase_article`,
arguments: [
tx.object(contentId),
tx.payment(ARTICLE_PRICE)
]
});
// Get sponsor signature
const sponsorSig = await requestSponsor({
transaction: tx,
user: readerAddress,
reason: 'article_purchase'
});
// Execute with both signatures
return await executeSponsored(tx, readerAddress, sponsorSig);
} else {
// Direct payment flow
return await executeDirectPurchase(contentId, readerAddress);
}
}
4. Content Access
// Verify and retrieve content
async function accessContent(
contentId: ObjectId,
readerAddress: Address
) {
// Client-side entitlement check
const entitlement = await checkEntitlement(readerAddress, contentId);
if (!entitlement.valid) {
throw new Error('No valid entitlement');
}
// Fetch content with CDN priority
const content = await fetchContent({
contentId,
sources: [
{ type: 'cdn', url: CDN_ENDPOINT },
{ type: 'walrus', url: WALRUS_AGGREGATOR }
]
});
// Verify integrity
if (hash(content) !== entitlement.contentHash) {
throw new Error('Content integrity check failed');
}
return content;
}
Key Features for Readers
- Zero Crypto Knowledge Required: zkLogin handles all blockchain complexity
- Instant Access: Sponsored transactions remove gas barriers
- Familiar Payment Models: Per-article or subscription, just like traditional media
- Privacy Protected: No personal information stored on-chain
- Resilient Delivery: CDN with Walrus fallback ensures availability
✍️ Journalist/Creator Persona
Profile
- Primary Goal: Monetize quality journalism fairly and transparently
- Technical Sophistication: Medium to high
- Crypto Experience: Comfortable with wallets and transactions
- Revenue Focus: Maximize earnings through direct reader relationships
- Collaboration Needs: Co-author splits and investigation bounties
User Journey
Technical Flow
1. Content Publishing
// Journalist publishes investigative piece with co-authors
async function publishInvestigation(
article: {
title: string;
content: string;
media: File[];
embargo?: Date;
},
coAuthors: {
address: Address;
royaltyShare: number;
}[]
) {
// Upload to Walrus via Quilt SDK
const quiltClient = new QuiltClient({
network: 'mainnet',
optimizeFor: 'large_media'
});
const blobId = await quiltClient.upload({
data: article.content,
media: article.media,
onProgress: (progress) => updateUploadUI(progress)
});
// Create Kiosk v2 policy with TransferPolicy
const policy = await createTransferPolicy({
rules: [
{
type: 'royalty_rule',
total_percentage: 15, // 15% total royalty
recipients: [journalist, ...coAuthors.map(a => a.address)],
shares: [70, ...coAuthors.map(a => a.royaltyShare)] // 70% to primary author
},
{
type: 'floor_price',
amount: ARTICLE_BASE_PRICE
},
{
type: 'time_lock',
embargo_until: article.embargo?.getTime()
}
]
});
// Mint content NFT
const tx = new TransactionBlock();
const contentNFT = tx.moveCall({
target: `${PACKAGE_ID}::content::mint`,
arguments: [
tx.object(JOURNALIST_KIOSK),
tx.pure({
title: article.title,
walrus_blob_id: blobId,
content_hash: hash(article),
transfer_policy: policy
})
]
});
const result = await signAndExecute(tx);
return extractContentId(result);
}
2. Bounty Management
// Create bounty for investigation sources
async function createInvestigationBounty(
investigation: {
topic: string;
requirements: string;
deadline: Date;
reward: number;
}
) {
const tx = new TransactionBlock();
// Escrow bounty funds
const [bounty] = tx.moveCall({
target: `${PACKAGE_ID}::bounty::create`,
arguments: [
tx.pure(investigation.topic),
tx.pure(hash(investigation.requirements)),
tx.pure(investigation.deadline.getTime()),
tx.payment(investigation.reward),
tx.pure({
min_reputation: 100,
require_encryption: true,
auto_resolve_after: 30 * 24 * 60 * 60 * 1000 // 30 days
})
]
});
// Emit bounty creation event
tx.moveCall({
target: `${PACKAGE_ID}::events::emit_bounty_created`,
arguments: [bounty]
});
return await signAndExecute(tx);
}
// Review and resolve bounty submissions
async function resolveBountySubmission(
bountyId: ObjectId,
submissionId: ObjectId,
decision: 'accept' | 'reject',
feedback?: string
) {
// Decrypt submission for review (requires journalist key)
const submission = await decryptSubmission(submissionId);
// Verify with Nautilus oracle if needed
if (submission.requires_verification) {
const oracleReceipt = await verifyWithNautilus({
submission,
enclave_measurement: NAUTILUS_MEASUREMENT,
verification_type: 'document_authenticity'
});
if (!oracleReceipt.valid) {
decision = 'reject';
}
}
// Resolve on-chain
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::bounty::resolve`,
arguments: [
tx.object(bountyId),
tx.object(submissionId),
tx.pure(decision),
tx.pure(feedback ? hash(feedback) : null)
]
});
return await signAndExecute(tx);
}
3. Revenue Management
// Track and withdraw earnings
async function manageRevenue(journalistAddress: Address) {
// Query on-chain revenue
const revenue = await getRevenueStats({
address: journalistAddress,
include: ['article_sales', 'royalties', 'bounty_costs']
});
// Automatic royalty distribution (handled by Kiosk)
// Manual withdrawal of accumulated earnings
if (revenue.available_balance > MIN_WITHDRAWAL) {
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::revenue::withdraw`,
arguments: [
tx.object(REVENUE_POOL),
tx.pure(revenue.available_balance)
]
});
await signAndExecute(tx);
}
return revenue;
}
Key Features for Journalists
- Direct Monetization: No intermediaries between journalists and readers
- Transparent Royalties: On-chain enforcement of revenue splits
- Investigation Tools: Bounty system for source management
- Content Ownership: NFT-based ownership with perpetual royalties
- Analytics Access: Real-time revenue and readership data
🔒 Source/Whistleblower Persona
Profile
- Primary Goal: Submit sensitive information securely and anonymously
- Technical Sophistication: Low to medium
- Anonymity Requirement: Critical (life/freedom may depend on it)
- Trust Model: Zero trust in any single party
- Motivation: Financial reward and/or public interest
User Journey
Technical Flow
1. Anonymous Access
// Source accesses platform anonymously
async function initializeAnonymousSession() {
// Generate temporary identity
const tempIdentity = {
id: generateRandomId(),
keypair: Ed25519Keypair.generate(),
expires: Date.now() + 24 * 60 * 60 * 1000 // 24 hours
};
// Store in session storage only (not persistent)
sessionStorage.setItem('temp_identity', encrypt(tempIdentity));
// Initialize anonymous browsing
return {
identity: tempIdentity.id,
canSubmit: true,
reputation: 0 // Start with no reputation
};
}
// Browse bounties without revealing identity
async function browseBountiesAnonymously() {
// Fetch from Walrus to avoid CDN tracking
const bounties = await fetchFromWalrus({
path: 'active_bounties_manifest',
anonymous: true
});
// Filter by risk level
return bounties.filter(bounty => {
return bounty.journalist_reputation > MIN_JOURNALIST_REP &&
bounty.encryption_required === true &&
bounty.allows_anonymous === true;
});
}
2. Secure Submission
// Submit documents with maximum privacy
async function submitWhistleblowerDocuments(
bountyId: string,
documents: File[],
journalistKey: PublicKey
) {
// Remove all metadata
const sanitizedDocs = await sanitizeDocuments(documents, {
remove_exif: true,
remove_timestamps: true,
remove_author_info: true,
add_noise: true // Add slight noise to prevent fingerprinting
});
// Encrypt with Seal (threshold encryption)
const sealClient = new SealClient({
keyServers: [
'ks1.whistleblower.protocolmedia.io',
'ks2.whistleblower.protocolmedia.io',
'ks3.whistleblower.protocolmedia.io',
'ks4.backup.protocolmedia.io',
'ks5.backup.protocolmedia.io'
],
threshold: 3 // Need 3 of 5 servers
});
const encrypted = await sealClient.encrypt({
data: sanitizedDocs,
policy: {
approvers: [journalistKey],
conditions: {
min_approvers: 1,
time_lock: Date.now() + 48 * 60 * 60 * 1000, // 48hr review period
dead_mans_switch: {
enabled: true,
trigger_after: 30 * 24 * 60 * 60 * 1000, // 30 days
auto_decrypt: true // Auto-reveal if no action taken
}
}
}
});
// Upload through anonymizing relay
const walrusId = await uploadViaOnionRoute({
data: encrypted,
hops: 3,
exit_node: 'walrus_aggregator'
});
// Anchor submission on-chain (only hash)
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::bounty::submit_anonymous`,
arguments: [
tx.object(bountyId),
tx.pure(hash(encrypted)),
tx.pure(walrusId),
tx.pure(tempIdentity.keypair.getPublicKey())
]
});
// Sign with temporary identity
return await signAndExecuteAnonymous(tx, tempIdentity);
}
3. Verification and Payout
// Handle verification without revealing identity
async function handleVerificationRequest(
submissionId: string,
verificationRequest: VerificationRequest
) {
// Provide zero-knowledge proof of authenticity
const zkProof = await generateAuthenticityProof({
documents: originalDocuments,
claim: verificationRequest.claim,
reveal: 'nothing' // Reveal no actual content
});
// Submit proof through Nautilus oracle
const oracleReceipt = await submitToNautilus({
proof: zkProof,
enclave: NAUTILUS_WHISTLEBLOWER_ENCLAVE,
attestation: {
type: 'document_authenticity',
confidence_threshold: 0.95
}
});
return oracleReceipt;
}
// Receive payout anonymously
async function receiveAnonymousPayout(
submissionId: string,
tempIdentity: TempIdentity
) {
// Generate one-time payout address
const payoutAddress = derivePayoutAddress(tempIdentity, submissionId);
// Claim payout to temporary address
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::bounty::claim_anonymous_payout`,
arguments: [
tx.object(submissionId),
tx.pure(payoutAddress)
]
});
await signAndExecuteAnonymous(tx, tempIdentity);
// Mix funds through privacy protocol
await mixFunds({
from: payoutAddress,
to: finalDestination,
mixing_rounds: 3
});
}
Key Features for Sources
- Complete Anonymity: No identity requirements, Tor support
- Threshold Encryption: No single point of failure with Seal
- Dead Man's Switch: Automatic revelation if journalist compromised
- Plausible Deniability: Encrypted data indistinguishable from random
- Reputation Without Identity: Build trust while remaining anonymous
Cross-User Interactions
Journalist ↔ Source Flow
Reader ↔ Journalist Flow
Security Considerations by User Type
Reader Security
- Minimal Attack Surface: Client-side verification only
- No Private Keys: zkLogin manages cryptography
- Session Isolation: Ephemeral keys per session
Journalist Security
- Wallet Security: Hardware wallet recommended
- Revenue Protection: Time-locked withdrawals
- Content Integrity: On-chain content hashes
Source Security
- Maximum Privacy: Onion routing, no cookies
- Encryption First: All data encrypted before transmission
- Minimal Metadata: Only hashes on-chain
Implementation Priorities
Phase 1: Reader Experience (Weeks 1-2)
- zkLogin integration
- Sponsored transaction system
- CDN/Walrus dual delivery
- Client-side entitlement checks
Phase 2: Journalist Tools (Weeks 3-4)
- Kiosk v2 with TransferPolicy
- Quilt SDK integration
- Bounty management system
- Revenue dashboard
Phase 3: Source Protection (Weeks 5-6)
- Seal threshold encryption
- Anonymous submission flow
- Nautilus oracle integration
- Reputation system
Success Metrics
Reader Metrics
- Time to first article: < 30 seconds
- Authentication success rate: > 95%
- Content load time: < 2 seconds
- Sponsored tx approval rate: > 90%
Journalist Metrics
- Publishing success rate: > 99%
- Revenue distribution accuracy: 100%
- Bounty resolution time: < 48 hours
- Royalty payment latency: < 1 hour
Source Metrics
- Anonymity preservation: 100%
- Submission success rate: > 95%
- Payout completion: < 24 hours
- Zero identity leaks: 0 incidents