Skip to content

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