Published May 18, 2026 · 24 min read

Building the Ultimate Procrastination Tool: 6 APIs of Distraction

APIs Humor Random Content Engagement JavaScript Mashup

1. Introduction: Embracing the Distraction

Every productivity tool promises to help you focus. Procrastination Station does the opposite. It is an unabashed celebration of wasted time, a six-API-powered engine of distraction that serves up an endless stream of jokes, useless facts, cat trivia, fox photos, corporate buzzword salad, and activity suggestions — all designed to keep you clicking instead of working.

The project began as a practical joke for a friend who was supposed to be studying for exams. The original version was just a Chuck Norris joke API with a "Give me another" button. But scope creep — the most honest form of procrastination — transformed it into a full-fledged content engine that mashes together six completely unrelated humor APIs into a single, devastatingly addictive interface.

From an engineering perspective, Procrastination Station is a study in engagement loops and content variety. The challenge is not making one API call (that is trivial). The challenge is making the 50th click as satisfying as the first. This requires careful thinking about content mixing, deduplication, animation timing, and the psychology of variable-ratio reinforcement schedules — the same principle that makes slot machines addictive.

This article walks through the complete build, from API selection to engagement optimization. We will cover each API in detail, explore the content mixing algorithm, implement a scoring and streak system, and discuss the surprisingly deep design decisions behind making random content feel curated. Whether you are building a humor app, a content feed, or any system that needs to keep users engaged with random data, the patterns here are broadly applicable.

2. The APIs Involved

2.1 Bored API

The Bored API suggests random activities when you have nothing to do. We covered this API in the Vibe Check article, so we will focus on what makes it different in the Procrastination Station context.

PropertyDetails
Base URLhttps://bored-api.appbrewery.com
AuthNone required
Rate LimitsNo documented limit
Response FormatJSON object
Key Endpoint/api/activity

In Procrastination Station, activity suggestions serve as gentle productivity nudges disguised as more procrastination. The irony is intentional: a tool designed to waste your time occasionally suggests you learn a new language or write a letter to a friend. These suggestions create small moments of genuine value within the larger distraction engine.

2.2 Chuck Norris Jokes API

The Chuck Norris Jokes API is a beloved internet classic. It serves random Chuck Norris "facts" from a curated database of user-submitted jokes.

PropertyDetails
Base URLhttps://api.chucknorris.io
AuthNone required
Rate LimitsNo documented limit
Response FormatJSON object
Key Endpoints/jokes/random, /jokes/random?category={cat}
{
  "icon_url": "https://api.chucknorris.io/img/avatar/chuck-norris.png",
  "id": "ae-78cogristij0ber_a",
  "url": "https://api.chucknorris.io/jokes/ae-78cogristij0ber_a",
  "value": "Chuck Norris can divide by zero.",
  "categories": ["dev"]
}

The API supports categories including "dev" (developer humor), "animal," "career," "food," "travel," and more. We use category filtering to vary the content over time and match the user's apparent interests. The joke database is large enough (roughly 600+ jokes) that deduplication is rarely needed in a single session.

2.3 Useless Facts API

The Useless Facts API serves random trivia that is genuinely interesting despite being objectively useless. It is the "Did you know?" engine of Procrastination Station.

PropertyDetails
Base URLhttps://uselessfacts.jsph.pl
AuthNone required
Rate LimitsNo documented limit
Response FormatJSON object
Key Endpoint/api/v2/facts/random?language=en
{
  "id": "a1b2c3d4e5f6",
  "text": "Honey never spoils. Archaeologists have found 3000-year-old honey in Egyptian tombs that was still edible.",
  "source": "https://en.wikipedia.org/wiki/Honey",
  "source_url": "https://en.wikipedia.org/wiki/Honey",
  "language": "en",
  "permalink": "https://uselessfacts.jsph.pl/api/v2/facts/a1b2c3d4e5f6"
}

The source_url field is useful — it provides a link for users who want to verify or learn more about a fact, creating an additional rabbit hole for procrastination.

2.4 Cat Facts API

Returning from its role in Vibe Check, Cat Facts provides feline trivia. In Procrastination Station it serves as a reliable content source with a distinct personality — cat people will keep clicking just for these.

PropertyDetails
Base URLhttps://catfact.ninja
AuthNone required
Key Endpoint/fact

2.5 RandomFox

Fox images break up the text-heavy feed with visual content. Each image is a tiny dopamine hit — cute animal photos are the atomic unit of internet distraction. In Procrastination Station, a fox image appears roughly every 3-4 content items, providing visual rhythm to the scroll.

2.6 Corporate BS Generator

The Corporate BS Generator produces randomly assembled corporate buzzword sentences. It is the most niche API in our stack but arguably the funniest for anyone who has endured corporate jargon.

PropertyDetails
Base URLhttps://corporatebs-generator.sameerkumar.website
AuthNone required
Rate LimitsNo documented limit
Response FormatJSON object
Key Endpoint/
{
  "phrase": "synergistically leverage existing bleeding-edge deliverables"
}

These phrases are procedurally generated from a grammar of corporate buzzwords, so the pool is essentially infinite. No deduplication needed. The phrases work beautifully as fake "motivational quotes" when displayed with a fancy card design and attributed to fictional executives.

3. Architecture & Data Flow

3.1 The Content Feed Pattern

Procrastination Station's architecture is fundamentally different from our other mashups. Instead of displaying all API results simultaneously, it presents content as an infinite feed — one item at a time, with a "Give me more" button that generates a new piece of content from a randomly selected API.

// The content feed is an array of items, each from a different API
const contentFeed = [];
let contentCounter = 0;

// Content type weights control the mix
const CONTENT_WEIGHTS = {
  chuckNorris: 20,
  uselessFact: 25,
  catFact: 15,
  foxImage: 10,
  boredActivity: 15,
  corporateBS: 15
};

function selectNextContentType() {
  const totalWeight = Object.values(CONTENT_WEIGHTS).reduce((sum, w) => sum + w, 0);
  let random = Math.random() * totalWeight;

  for (const [type, weight] of Object.entries(CONTENT_WEIGHTS)) {
    random -= weight;
    if (random <= 0) return type;
  }

  return 'uselessFact'; // Default fallback
}

The weights determine the probability of each content type appearing. Useless facts have the highest weight (25%) because they have the broadest appeal. Fox images have the lowest (10%) to keep the feed primarily text-based with occasional visual breaks.

3.2 The Anti-Repetition System

Nothing kills engagement faster than seeing the same joke twice in a row. We implement a "cooldown" system that prevents any content type from appearing consecutively and tracks recently shown items:

const recentTypes = []; // Track last 3 content types
const seenContent = new Set(); // Track all seen content by hash

function selectNextContentTypeWithCooldown() {
  let type;
  let attempts = 0;

  do {
    type = selectNextContentType();
    attempts++;
  } while (recentTypes.slice(-2).includes(type) && attempts < 20);
  // Prevent the same type from appearing 3 times in a row

  recentTypes.push(type);
  if (recentTypes.length > 5) recentTypes.shift();

  return type;
}

function isContentSeen(content) {
  const hash = simpleHash(typeof content === 'string' ? content : JSON.stringify(content));
  if (seenContent.has(hash)) return true;
  seenContent.add(hash);
  return false;
}

function simpleHash(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // Convert to 32bit integer
  }
  return hash.toString(36);
}

3.3 The Engagement Loop

The core loop is deceptively simple: click button, get content. But several subtle design decisions make this loop sticky:

async function generateNext() {
  const type = selectNextContentTypeWithCooldown();
  const button = document.getElementById('more-btn');

  // Immediate feedback: disable button and show anticipation
  button.disabled = true;
  button.textContent = getLoadingMessage(type);

  try {
    let content;
    let attempts = 0;

    // Retry loop for deduplication
    do {
      content = await fetchContent(type);
      attempts++;
    } while (isContentSeen(content.text || content.url) && attempts < 3);

    // Add to feed with animation
    const item = createFeedItem(type, content, ++contentCounter);
    prependToFeed(item);

    // Update streak and score
    updateStreak();
    updateScore(type);

  } catch (err) {
    // If one API fails, try a different type
    const fallbackType = selectNextContentTypeWithCooldown();
    try {
      const content = await fetchContent(fallbackType);
      const item = createFeedItem(fallbackType, content, ++contentCounter);
      prependToFeed(item);
    } catch (fallbackErr) {
      showTemporaryError('Even the distraction APIs are distracted. Try again.');
    }
  } finally {
    button.disabled = false;
    button.textContent = getNextButtonText();
  }
}

Key engagement details: the loading message changes based on the content type being fetched ("Searching for wisdom..." for facts, "Consulting Chuck..." for jokes), creating anticipation. New content is prepended (appears at the top) rather than appended, so users do not need to scroll. The button text varies randomly after each click, preventing visual habituation.

3.4 The Scoring System

To gamify the procrastination (yes, we are gamifying wasting time), we implement a simple scoring system that rewards continued engagement:

const gameState = {
  score: 0,
  streak: 0,
  longestStreak: 0,
  contentCounts: {
    chuckNorris: 0,
    uselessFact: 0,
    catFact: 0,
    foxImage: 0,
    boredActivity: 0,
    corporateBS: 0
  },
  lastClickTime: null,
  sessionStart: Date.now(),
  achievements: []
};

function updateScore(type) {
  // Base points per content type
  const pointMap = {
    chuckNorris: 10,
    uselessFact: 15,
    catFact: 12,
    foxImage: 8,
    boredActivity: 20, // Ironic: most points for the "productive" suggestion
    corporateBS: 5
  };

  let points = pointMap[type] || 10;

  // Streak multiplier
  if (gameState.streak > 5) points *= 1.5;
  if (gameState.streak > 15) points *= 2;
  if (gameState.streak > 30) points *= 3;

  gameState.score += Math.round(points);
  gameState.contentCounts[type]++;

  // Check achievements
  checkAchievements();

  renderScoreboard();
}

function updateStreak() {
  const now = Date.now();
  if (gameState.lastClickTime && (now - gameState.lastClickTime) < 30000) {
    gameState.streak++;
  } else {
    gameState.streak = 1;
  }

  if (gameState.streak > gameState.longestStreak) {
    gameState.longestStreak = gameState.streak;
  }

  gameState.lastClickTime = now;
}

3.5 The Achievement System

const ACHIEVEMENTS = [
  { id: 'first-click', name: 'First Distraction', desc: 'Generated your first piece of content', check: s => s.score > 0 },
  { id: 'ten-clicks', name: 'Getting Warmed Up', desc: '10 items generated', check: s => Object.values(s.contentCounts).reduce((a, b) => a + b) >= 10 },
  { id: 'fifty-clicks', name: 'Professional Procrastinator', desc: '50 items generated', check: s => Object.values(s.contentCounts).reduce((a, b) => a + b) >= 50 },
  { id: 'hundred-clicks', name: 'Master of Time Waste', desc: '100 items generated', check: s => Object.values(s.contentCounts).reduce((a, b) => a + b) >= 100 },
  { id: 'streak-10', name: 'On a Roll', desc: 'Maintained a 10-click streak', check: s => s.longestStreak >= 10 },
  { id: 'streak-30', name: 'Unstoppable', desc: 'Maintained a 30-click streak', check: s => s.longestStreak >= 30 },
  { id: 'all-types', name: 'Completionist', desc: 'Saw every content type at least once', check: s => Object.values(s.contentCounts).every(c => c > 0) },
  { id: 'cat-lover', name: 'Cat Person', desc: 'Viewed 20 cat facts', check: s => s.contentCounts.catFact >= 20 },
  { id: 'five-minutes', name: 'Time Flies', desc: 'Spent 5 minutes procrastinating', check: s => (Date.now() - s.sessionStart) > 300000 },
  { id: 'thirty-minutes', name: 'Where Did the Time Go?', desc: 'Spent 30 minutes procrastinating', check: s => (Date.now() - s.sessionStart) > 1800000 },
];

function checkAchievements() {
  ACHIEVEMENTS.forEach(achievement => {
    if (!gameState.achievements.includes(achievement.id) && achievement.check(gameState)) {
      gameState.achievements.push(achievement.id);
      showAchievementToast(achievement);
    }
  });
}

4. Complete Code Walkthrough

4.1 Content Fetchers

Each API gets a dedicated fetcher that normalizes the response into a common content format:

async function fetchContent(type) {
  const fetchers = {
    chuckNorris: fetchChuckNorris,
    uselessFact: fetchUselessFact,
    catFact: fetchCatFact,
    foxImage: fetchFoxImage,
    boredActivity: fetchBoredActivity,
    corporateBS: fetchCorporateBS
  };

  const fetcher = fetchers[type];
  if (!fetcher) throw new Error(`Unknown content type: ${type}`);

  return fetcher();
}

async function fetchChuckNorris() {
  const categories = ['dev', 'science', 'animal', 'career', 'food', 'travel'];
  const category = categories[Math.floor(Math.random() * categories.length)];

  const response = await fetchWithTimeout(
    `https://api.chucknorris.io/jokes/random?category=${category}`,
    { timeout: 5000 }
  );

  if (!response.ok) throw new Error(`Chuck Norris API returned ${response.status}`);

  const data = await response.json();
  return {
    type: 'joke',
    text: data.value,
    category: category,
    source: 'Chuck Norris Facts',
    icon: '🥋',
    url: data.url
  };
}

async function fetchUselessFact() {
  const response = await fetchWithTimeout(
    'https://uselessfacts.jsph.pl/api/v2/facts/random?language=en',
    { timeout: 5000 }
  );

  if (!response.ok) throw new Error(`Useless Facts API returned ${response.status}`);

  const data = await response.json();
  return {
    type: 'fact',
    text: data.text,
    source: 'Useless Facts',
    sourceUrl: data.source_url,
    icon: '🧠'
  };
}

async function fetchCatFact() {
  const response = await fetchWithTimeout(
    'https://catfact.ninja/fact',
    { timeout: 5000 }
  );

  if (!response.ok) throw new Error(`Cat Facts API returned ${response.status}`);

  const data = await response.json();
  return {
    type: 'fact',
    text: data.fact,
    source: 'Cat Facts',
    icon: '🐱'
  };
}

async function fetchFoxImage() {
  const response = await fetchWithTimeout(
    'https://randomfox.ca/floof/',
    { timeout: 5000 }
  );

  if (!response.ok) throw new Error(`RandomFox returned ${response.status}`);

  const data = await response.json();
  return {
    type: 'image',
    url: data.image,
    source: 'RandomFox',
    icon: '🦊',
    text: getRandomFoxCaption()
  };
}

function getRandomFoxCaption() {
  const captions = [
    'This fox is also procrastinating.',
    'Foxes: nature\'s professional procrastinators.',
    'This fox has not checked their email in three days.',
    'Productivity? This fox has never heard of it.',
    'Studies show looking at foxes increases procrastination by 47%.',
    'This fox was supposed to hunt an hour ago.',
    'Even this fox is more productive than you right now.',
    'Fox fact: they are excellent at avoiding responsibilities too.',
    'This fox just remembered they had a deadline yesterday.',
    'Cute animal break: justified procrastination since 2003.'
  ];
  return captions[Math.floor(Math.random() * captions.length)];
}

async function fetchBoredActivity() {
  const response = await fetchWithTimeout(
    'https://bored-api.appbrewery.com/api/activity',
    { timeout: 5000 }
  );

  if (!response.ok) throw new Error(`Bored API returned ${response.status}`);

  const data = await response.json();
  return {
    type: 'activity',
    text: data.activity,
    activityType: data.type,
    participants: data.participants,
    price: data.price,
    source: 'Activity Suggestion',
    icon: '🎯'
  };
}

async function fetchCorporateBS() {
  const response = await fetchWithTimeout(
    'https://corporatebs-generator.sameerkumar.website/',
    { timeout: 5000 }
  );

  if (!response.ok) throw new Error(`Corporate BS returned ${response.status}`);

  const data = await response.json();

  const fakeAuthors = [
    'VP of Synergy', 'Chief Disruption Officer', 'Head of Dynamic Solutions',
    'Director of Forward-Thinking Initiatives', 'SVP of Paradigm Shifts',
    'Manager of Actionable Insights', 'Lead Thought Partner',
    'Principal Innovation Evangelist', 'Executive Buzzword Strategist'
  ];

  return {
    type: 'quote',
    text: data.phrase,
    author: fakeAuthors[Math.floor(Math.random() * fakeAuthors.length)],
    source: 'Corporate Wisdom',
    icon: '👔'
  };
}

4.2 Feed Item Rendering

function createFeedItem(type, content, number) {
  const timestamp = new Date().toLocaleTimeString();

  let cardHTML;

  switch (content.type) {
    case 'joke':
      cardHTML = `
        <div class="feed-item joke-item" data-number="${number}">
          <div class="item-header">
            <span class="item-icon">${content.icon}</span>
            <span class="item-source">${content.source}</span>
            <span class="item-number">#${number}</span>
          </div>
          <p class="item-text">${escapeHTML(content.text)}</p>
          <div class="item-footer">
            <span class="item-time">${timestamp}</span>
            <span class="item-category">${content.category || ''}</span>
          </div>
        </div>`;
      break;

    case 'image':
      cardHTML = `
        <div class="feed-item image-item" data-number="${number}">
          <div class="item-header">
            <span class="item-icon">${content.icon}</span>
            <span class="item-source">${content.source}</span>
            <span class="item-number">#${number}</span>
          </div>
          <div class="image-container">
            <img src="${content.url}" alt="Random fox" loading="lazy"
                 onload="this.classList.add('loaded')">
          </div>
          <p class="item-caption">${escapeHTML(content.text)}</p>
        </div>`;
      break;

    case 'quote':
      cardHTML = `
        <div class="feed-item quote-item" data-number="${number}">
          <div class="item-header">
            <span class="item-icon">${content.icon}</span>
            <span class="item-source">${content.source}</span>
            <span class="item-number">#${number}</span>
          </div>
          <blockquote class="bs-quote">
            <p>"${capitalizeFirst(escapeHTML(content.text))}"</p>
            <cite>— ${content.author}</cite>
          </blockquote>
        </div>`;
      break;

    case 'activity':
      cardHTML = `
        <div class="feed-item activity-item" data-number="${number}">
          <div class="item-header">
            <span class="item-icon">${content.icon}</span>
            <span class="item-source">${content.source}</span>
            <span class="item-number">#${number}</span>
          </div>
          <p class="item-text">${escapeHTML(content.text)}</p>
          <div class="activity-meta">
            <span>Type: ${content.activityType}</span>
            <span>People: ${content.participants}</span>
            <span>Cost: ${content.price === 0 ? 'Free' : '$'.repeat(Math.ceil(content.price * 4))}</span>
          </div>
        </div>`;
      break;

    default:
      cardHTML = `
        <div class="feed-item fact-item" data-number="${number}">
          <div class="item-header">
            <span class="item-icon">${content.icon}</span>
            <span class="item-source">${content.source}</span>
            <span class="item-number">#${number}</span>
          </div>
          <p class="item-text">${escapeHTML(content.text)}</p>
          ${content.sourceUrl ? `<a href="${content.sourceUrl}" target="_blank" rel="noopener" class="source-link">Learn more</a>` : ''}
        </div>`;
  }

  return cardHTML;
}

function escapeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

function prependToFeed(itemHTML) {
  const feed = document.getElementById('content-feed');
  const wrapper = document.createElement('div');
  wrapper.innerHTML = itemHTML;
  const item = wrapper.firstElementChild;

  item.style.opacity = '0';
  item.style.transform = 'translateY(-20px)';

  feed.prepend(item);

  // Trigger animation
  requestAnimationFrame(() => {
    item.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
    item.style.opacity = '1';
    item.style.transform = 'translateY(0)';
  });

  // Limit feed size to prevent memory issues
  while (feed.children.length > 50) {
    feed.lastElementChild.remove();
  }
}

4.3 Dynamic Button Text

function getNextButtonText() {
  const texts = [
    'Distract Me Again', 'One More...', 'I Should Be Working But...',
    'Just One More Click', 'Feed the Procrastination',
    'What Else Ya Got?', 'Keep Going', 'My Deadline Can Wait',
    'More Please', 'I Regret Nothing', 'Still Not Working',
    'Give Me More Distractions', 'Another One',
    'This Is Fine', 'Who Needs Productivity?',
    'Click Here Instead of Working', 'Embrace the Void'
  ];

  return texts[Math.floor(Math.random() * texts.length)];
}

function getLoadingMessage(type) {
  const messages = {
    chuckNorris: 'Consulting Chuck...',
    uselessFact: 'Mining useless knowledge...',
    catFact: 'Asking the cats...',
    foxImage: 'Finding a photogenic fox...',
    boredActivity: 'Generating excuses...',
    corporateBS: 'Synergizing deliverables...'
  };

  return messages[type] || 'Loading distraction...';
}

5. Challenges & Gotchas

5.1 Content Filtering

User-generated content (like Chuck Norris jokes) occasionally contains inappropriate material. The Chuck Norris API has an "explicit" category that we exclude, but even non-explicit categories sometimes contain jokes that push boundaries. We implement a basic content filter:

const BLOCKED_PATTERNS = [
  // Patterns we want to filter out (kept deliberately vague here)
  /\b(offensive|pattern|here)\b/i
];

function isContentAppropriate(text) {
  return !BLOCKED_PATTERNS.some(pattern => pattern.test(text));
}

// In the fetch functions, add filtering:
async function fetchChuckNorrisFiltered() {
  let attempts = 0;
  let content;

  do {
    content = await fetchChuckNorris();
    attempts++;
  } while (!isContentAppropriate(content.text) && attempts < 5);

  return content;
}

5.2 API Reliability and Graceful Degradation

With six APIs, the probability that at least one is down at any given time is non-trivial. If each API has 99% uptime, the probability that all six are up simultaneously is 0.99^6 = 94%. That means about 6% of the time, at least one API will be unavailable.

Our fallback strategy: if the selected API fails, we try a different random API. If that also fails, we serve content from a local fallback pool. This pool contains pre-fetched content from previous successful requests, stored in sessionStorage:

const FALLBACK_POOL_KEY = 'procrastination-fallback-pool';

function saveFallbackContent(content) {
  const pool = JSON.parse(sessionStorage.getItem(FALLBACK_POOL_KEY) || '[]');
  pool.push(content);

  // Keep pool manageable
  if (pool.length > 100) pool.shift();

  sessionStorage.setItem(FALLBACK_POOL_KEY, JSON.stringify(pool));
}

function getRandomFallbackContent() {
  const pool = JSON.parse(sessionStorage.getItem(FALLBACK_POOL_KEY) || '[]');
  if (pool.length === 0) return null;

  const index = Math.floor(Math.random() * pool.length);
  return pool[index];
}

5.3 The Corporate BS API Reliability

The Corporate BS Generator is the least reliable API in our stack. It is a small personal project hosted on what appears to be a shared server. Downtime is not uncommon. For this specific API, we implement a local generator as a fallback:

function generateLocalCorporateBS() {
  const verbs = ['leverage', 'synergize', 'optimize', 'disrupt', 'streamline', 'revolutionize',
    'transform', 'iterate', 'scale', 'innovate', 'orchestrate', 'facilitate', 'empower'];
  const adjectives = ['cutting-edge', 'next-generation', 'best-in-class', 'world-class',
    'bleeding-edge', 'mission-critical', 'results-driven', 'forward-thinking',
    'data-driven', 'customer-centric', 'agile', 'holistic', 'seamless'];
  const nouns = ['synergies', 'paradigms', 'deliverables', 'ecosystems', 'mindshare',
    'bandwidth', 'solutions', 'verticals', 'value propositions', 'core competencies',
    'thought leadership', 'best practices', 'action items', 'stakeholder alignment'];

  const verb = verbs[Math.floor(Math.random() * verbs.length)];
  const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
  const noun = nouns[Math.floor(Math.random() * nouns.length)];

  return `${verb} ${adj} ${noun}`;
}

5.4 Memory Management for Long Sessions

Procrastination Station is designed for extended use. Users might generate hundreds of items in a single session, and each feed item is a DOM element. Without pruning, the DOM can grow large enough to cause performance issues:

// In prependToFeed, we already limit to 50 items
while (feed.children.length > 50) {
  feed.lastElementChild.remove();
}

// Additionally, periodically clear the seen content set
setInterval(() => {
  if (seenContent.size > 500) {
    // Keep only the most recent 200 entries
    const entries = Array.from(seenContent);
    seenContent.clear();
    entries.slice(-200).forEach(e => seenContent.add(e));
  }
}, 60000);

5.5 Mobile Touch Interactions

On mobile, the "Give me more" button needs to be large enough for comfortable tapping, and feed items need swipe-friendly spacing. We also add haptic feedback via the Vibration API for a satisfying physical sensation on each new item:

function hapticFeedback() {
  if ('vibrate' in navigator) {
    navigator.vibrate(10); // Subtle 10ms vibration
  }
}

6. Real-World Use Cases

6.1 Team Building and Ice Breakers

Procrastination Station works surprisingly well as a meeting warm-up tool. Project the feed on a screen, hit the button a few times, and let the random content spark conversation. Chuck Norris jokes get laughs. Useless facts prompt discussions. Corporate BS quotes create in-jokes about your own company's jargon.

6.2 Waiting Room Entertainment

Medical offices, auto repair shops, government offices — anywhere people wait could benefit from an idle distraction screen. The content is family-friendly (with filtering), requires no user accounts, and provides genuinely varied entertainment. The scoring system gives waiting visitors a small game to play.

6.3 Content Discovery Prototyping

The feed pattern, engagement scoring, and content mixing algorithm are directly applicable to any content discovery product. Strip away the joke APIs and replace them with your actual content sources (articles, products, videos), and you have a working prototype of a recommendation engine with engagement tracking.

6.4 Teaching Engagement Design

The project is a case study in engagement mechanics: variable-ratio reinforcement (random content types), streak rewards, achievement unlocks, and loss aversion (the streak counter). These patterns are identical to those used in major social media and gaming platforms. Understanding them through a transparent, small-scale implementation helps developers think critically about engagement design in their own products.

6.5 Creative Writing Prompts

The random juxtaposition of Chuck Norris jokes, cat facts, corporate jargon, and activity suggestions creates unexpected creative prompts. A writing exercise could challenge participants to incorporate all six content types from a single session into a coherent short story.

6.6 Digital Signage

The auto-refresh variant of Procrastination Station (where content generates on a timer instead of a button click) works well as digital signage. Office lobbies, co-working spaces, or break rooms can display a rotating feed of fun facts and images that updates throughout the day.

7. Performance Optimization

7.1 Prefetching the Next Item

The most impactful optimization is prefetching. While the user reads the current item, we quietly fetch the next one in the background. This makes each click feel instantaneous:

let prefetchedContent = null;
let prefetchedType = null;

function prefetchNext() {
  const type = selectNextContentTypeWithCooldown();
  prefetchedType = type;

  fetchContent(type)
    .then(content => { prefetchedContent = content; })
    .catch(() => { prefetchedContent = null; });
}

async function generateNext() {
  let content, type;

  if (prefetchedContent) {
    content = prefetchedContent;
    type = prefetchedType;
    prefetchedContent = null;
    prefetchedType = null;
  } else {
    type = selectNextContentTypeWithCooldown();
    content = await fetchContent(type);
  }

  // Render content...

  // Start prefetching the next item immediately
  prefetchNext();
}

7.2 Image Preloading

Fox images can be slow to load. We preload the next fox image so it appears instantly when drawn:

let preloadedFoxUrl = null;

function preloadFoxImage() {
  fetchFoxImage().then(content => {
    const img = new Image();
    img.src = content.url;
    preloadedFoxUrl = content.url;
  }).catch(() => {});
}

// Preload on startup and after each fox image is shown
preloadFoxImage();

7.3 Batch Prefetching

For an even smoother experience, we can prefetch a queue of 3-5 items:

const prefetchQueue = [];
const MAX_QUEUE_SIZE = 5;

async function fillPrefetchQueue() {
  while (prefetchQueue.length < MAX_QUEUE_SIZE) {
    const type = selectNextContentType();
    try {
      const content = await fetchContent(type);
      prefetchQueue.push({ type, content });
    } catch {
      break; // Stop filling on error
    }
  }
}

// Fill queue during idle time
requestIdleCallback(() => fillPrefetchQueue());

7.4 Animation Performance

Feed item animations use CSS transforms and opacity, which are GPU-accelerated and do not trigger layout recalculations. We avoid animating properties like height, width, or margin that force expensive reflows:

.feed-item {
  will-change: transform, opacity;
  transform: translateZ(0); /* Force GPU layer */
}

7.5 Score Persistence

We persist the game state to localStorage so users can leave and return without losing their score. This creates a subtle retention hook — they have a score to protect:

function saveGameState() {
  localStorage.setItem('procrastination-state', JSON.stringify(gameState));
}

function loadGameState() {
  const saved = localStorage.getItem('procrastination-state');
  if (saved) {
    Object.assign(gameState, JSON.parse(saved));
    // Reset session-specific values
    gameState.streak = 0;
    gameState.sessionStart = Date.now();
  }
}

// Auto-save every 10 seconds
setInterval(saveGameState, 10000);

8. Extending the Tool

8.1 Social Sharing

Add a "Share" button on each content card that copies the text to clipboard or generates a shareable image. This turns individual pieces of content into social media posts and drives organic traffic back to the tool. The Web Share API makes this native on mobile:

async function shareContent(content) {
  if (navigator.share) {
    await navigator.share({
      title: 'Procrastination Station',
      text: content.text,
      url: window.location.href
    });
  } else {
    await navigator.clipboard.writeText(content.text);
    showToast('Copied to clipboard!');
  }
}

8.2 Multiplayer Procrastination

Add a real-time leaderboard using a simple WebSocket or Firebase connection. Users compete for the highest procrastination score, creating a competitive dynamic around doing nothing productive. The irony is the point.

8.3 Themed Content Modes

Add toggles for different content themes: "Developer Mode" (only dev jokes, coding facts), "Office Mode" (corporate BS and career activities), "Animal Mode" (cat facts and fox images only). This lets users customize their distraction experience.

8.4 Daily Digest

Save the best items from each session and compile a "Daily Digest" email or notification with the top 5 items. This brings users back even when they are being productive, which is the ultimate procrastination engineering.

8.5 Integration with More Joke APIs

The joke ecosystem is vast. Add the Official Joke API, Dad Jokes API, Programming Jokes API, or the Kanye West quote generator to increase content variety. Each new API source reduces repetition and adds a new flavor to the feed.

8.6 Productivity Guilt Timer

Add a passive-aggressive timer that counts how long you have been procrastinating and periodically displays messages like "You have been procrastinating for 23 minutes. Your deadline has not moved." This self-aware humor adds another dimension to the experience.

8.7 Content Rating System

Let users rate content with thumbs up or down. Use the ratings to adjust content weights dynamically — if a user consistently likes cat facts and dislikes corporate BS, shift the probabilities accordingly. This creates a personalized distraction engine that gets better over time.

9. Conclusion

Procrastination Station is, by design, the most frivolous project in the APIMashupHub collection. It serves no productive purpose. It solves no real problem. It is engineered specifically to waste your time as effectively as possible.

And that is exactly what makes it instructive. The engagement patterns in Procrastination Station — variable-ratio content delivery, streak mechanics, achievement unlocks, prefetching for instant gratification, anti-repetition algorithms, graceful degradation for reliability, and persistent scoring for retention — are the same patterns that power every major content platform, social network, and mobile game. By building them transparently in a small project, we can see these mechanics clearly, understand them deeply, and apply them responsibly in our own work.

The technical story is equally valuable. Six APIs, each with different response formats, different reliability characteristics, and different content types, unified into a single coherent feed through a content mixing algorithm, a fallback hierarchy, and a shared rendering pipeline. This is a microcosm of any content aggregation system: you have sources, you have mixing logic, you have presentation, and you have engagement tracking. The scale is tiny but the architecture is real.

The six APIs are all free, all open, and all genuinely fun. The architecture handles failures gracefully. The engagement loop is satisfyingly addictive. And the entire thing can be built in a weekend. That is the beauty of API mashups: the components are commodities but the combinations are infinite.

Now stop reading and go be productive. Or click the button one more time. We both know which one you are going to choose.


Try Procrastination Station: Launch the tool

APIs used: Bored API · Chuck Norris · Useless Facts · Cat Facts · RandomFox · Corporate BS

Related posts: Building a Mood-Based API Engine · Crypto Meets Meteorology