feeds.createAccessLink()
Create a temporary private access link for a feed entry. This allows authorized users to access paid content.
Signature
feeds.createAccessLink(feedId: string, entryId: string): Promise<AccessLink>Parameters
feedId
- Type:
string - Required: Yes
- Format: UUID
The unique identifier of the feed.
entryId
- Type:
string - Required: Yes
- Format: UUID
The unique identifier of the entry.
Returns
- Type:
Promise<AccessLink>
interface AccessLink {
url: string; // Temporary access URL
expires_at: number; // Unix timestamp when link expires
}Usage
Basic Example
const accessLink = await grapevine.feeds.createAccessLink(feedId, entryId);
console.log('Access URL:', accessLink.url);
console.log('Expires:', new Date(accessLink.expires_at * 1000).toLocaleString());Download Content
async function downloadContent(feedId: string, entryId: string) {
// Create access link
const accessLink = await grapevine.feeds.createAccessLink(feedId, entryId);
// Fetch the content
const response = await fetch(accessLink.url);
const content = await response.text();
return content;
}With Expiration Check
async function getAccessWithExpiry(feedId: string, entryId: string) {
const accessLink = await grapevine.feeds.createAccessLink(feedId, entryId);
const expiresAt = new Date(accessLink.expires_at * 1000);
const now = new Date();
const secondsRemaining = Math.floor((expiresAt.getTime() - now.getTime()) / 1000);
return {
url: accessLink.url,
expiresAt,
secondsRemaining,
isExpired: secondsRemaining <= 0
};
}Content Viewer Component
function ContentViewer({ feedId, entryId }: { feedId: string; entryId: string }) {
const [content, setContent] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadContent() {
try {
setLoading(true);
// Create access link
const accessLink = await grapevine.feeds.createAccessLink(feedId, entryId);
// Fetch content
const response = await fetch(accessLink.url);
if (!response.ok) {
throw new Error('Failed to fetch content');
}
const data = await response.text();
setContent(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadContent();
}, [feedId, entryId]);
if (loading) return <div>Loading content...</div>;
if (error) return <div>Error: {error}</div>;
return <div className="content">{content}</div>;
}Image Viewer with Access Link
function ProtectedImage({ feedId, entryId }: { feedId: string; entryId: string }) {
const [imageUrl, setImageUrl] = useState<string | null>(null);
useEffect(() => {
grapevine.feeds.createAccessLink(feedId, entryId)
.then(link => setImageUrl(link.url))
.catch(console.error);
}, [feedId, entryId]);
if (!imageUrl) return <div>Loading image...</div>;
return <img src={imageUrl} alt="Protected content" />;
}Caching Access Links
class AccessLinkCache {
private cache = new Map<string, { url: string; expiresAt: number }>();
async getAccessLink(
grapevine: GrapevineClient,
feedId: string,
entryId: string
): Promise<string> {
const key = `${feedId}:${entryId}`;
const cached = this.cache.get(key);
// Return cached if not expired (with 60s buffer)
if (cached && cached.expiresAt > Date.now() / 1000 + 60) {
return cached.url;
}
// Create new access link
const accessLink = await grapevine.feeds.createAccessLink(feedId, entryId);
this.cache.set(key, {
url: accessLink.url,
expiresAt: accessLink.expires_at
});
return accessLink.url;
}
clearExpired() {
const now = Date.now() / 1000;
for (const [key, value] of this.cache.entries()) {
if (value.expiresAt <= now) {
this.cache.delete(key);
}
}
}
}
// Usage
const cache = new AccessLinkCache();
const url = await cache.getAccessLink(grapevine, feedId, entryId);Download with Retry
async function downloadWithRetry(
feedId: string,
entryId: string,
maxRetries: number = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Get fresh access link
const accessLink = await grapevine.feeds.createAccessLink(feedId, entryId);
// Download content
const response = await fetch(accessLink.url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.blob();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
// Wait before retry
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
}Error Handling
try {
const accessLink = await grapevine.feeds.createAccessLink(feedId, entryId);
console.log('Access link created:', accessLink.url);
} catch (error) {
if (error.message.includes('401')) {
console.error('Authentication required');
} else if (error.message.includes('403')) {
console.error('Not authorized to access this content');
} else if (error.message.includes('404')) {
console.error('Feed or entry not found');
} else {
console.error('Error:', error.message);
}
}Notes
- Authentication: Required - must be authenticated with wallet signature
- Expiration: Links are temporary and expire after a short period
- Payment: May require payment for paid content (handled via x402)
- One-time Use: Some implementations may limit link usage
Related
- entries.get() - Get entry metadata
- entries.list() - List feed entries
- API: Access Link - REST API reference