diff --git a/components/dashboard/NavBar.tsx b/components/dashboard/NavBar.tsx
index af25a5f..6d3277f 100644
--- a/components/dashboard/NavBar.tsx
+++ b/components/dashboard/NavBar.tsx
@@ -24,7 +24,7 @@ function NavBar() {
             variants={navIconVariants}
             className="pr-5 lg:pr-0 lg:pt-3 lg:pb-3"
           >
-            <ActiveLink href="/dashboard/ranking">
+            <ActiveLink href="/ranking">
               <RankingIcon />
             </ActiveLink>
           </m.div>
diff --git a/layouts/DashLayout.tsx b/layouts/DashLayout.tsx
index 9235338..612004c 100644
--- a/layouts/DashLayout.tsx
+++ b/layouts/DashLayout.tsx
@@ -8,7 +8,6 @@ import {
 import Head from "next/head";
 import { useRouter } from "next/router";
 import NavBar from "../components/dashboard/NavBar";
-import { NavTemplate } from "./NavTemplates";
 
 interface DashLayoutProps {
   children: React.ReactNode;
@@ -26,6 +25,7 @@ function DashLayout(props: DashLayoutProps) {
       variants={containerVariants}
     >
       <Head>
+        <meta name="viewport" content="initial-scale=0.8" />
         <title>Dashboard - toffee</title>
         <meta name="description" content="Dashboard statistics for toffee" />
         <link rel="icon" href="/favicon.ico" />
diff --git a/misc/7TVAPI.tsx b/misc/7TVAPI.tsx
index b7e6e78..5ffebf3 100644
--- a/misc/7TVAPI.tsx
+++ b/misc/7TVAPI.tsx
@@ -1,8 +1,7 @@
-import Redis from "ioredis";
-
-let redis = new Redis(process.env.REDIS_URL);
+import type RedisInstance from "ioredis";
 
 async function applyCache(
+  redis: RedisInstance,
   key: string,
   query: string,
   gql: boolean,
@@ -11,7 +10,7 @@ async function applyCache(
   if (await redis.get(key)) {
     return JSON.parse((await redis.get(key)) as string);
   } else {
-    const response = await fetchEndpoint(query, gql);
+    const response = await fetchEndpoint(redis, query, gql);
     if (response != null) {
       await redis.set(key, JSON.stringify(response), "EX", cacheTime);
     }
@@ -19,7 +18,11 @@ async function applyCache(
   }
 }
 
-async function fetchEndpoint(query: string, gql: boolean = false) {
+async function fetchEndpoint(
+  redis: RedisInstance,
+  query: string,
+  gql: boolean = false
+) {
   if (await redis.get("7TV.RATE_LIMIT")) {
     await new Promise((resolve) => setTimeout(resolve, 1000));
   } else {
@@ -50,7 +53,7 @@ async function fetchGQL(query: string) {
   return json;
 }
 
-async function getGlobalEmotes() {
+async function getGlobalEmotes(redis: RedisInstance) {
   const gqlQuery = `query {
         namedEmoteSet(name: GLOBAL) {
             emote_count
@@ -78,10 +81,10 @@ async function getGlobalEmotes() {
             }
         }
     }`;
-  return await applyCache("7TV.GLOBAL_EMOTES", gqlQuery, true, 3600);
+  return await applyCache(redis, "7TV.GLOBAL_EMOTES", gqlQuery, true, 3600);
 }
 
-async function getChannelEmotes(channelID: string) {
+async function getChannelEmotes(redis: RedisInstance, channelID: string) {
   const gqlQuery = `query {
         user(id: "${channelID}") {
             emote_sets {
@@ -112,6 +115,7 @@ async function getChannelEmotes(channelID: string) {
         }
     }`;
   return await applyCache(
+    redis,
     "7TV.CHANNEL_EMOTES_" + channelID,
     gqlQuery,
     true,
diff --git a/misc/BTTVAPI.tsx b/misc/BTTVAPI.tsx
new file mode 100644
index 0000000..675c58c
--- /dev/null
+++ b/misc/BTTVAPI.tsx
@@ -0,0 +1,58 @@
+import type RedisInstance from "ioredis";
+
+async function applyCache(
+  redis: RedisInstance,
+  key: string,
+  query: string,
+
+  cacheTime: number
+) {
+  if (await redis.get(key)) {
+    return JSON.parse((await redis.get(key)) as string);
+  } else {
+    const response = await fetchEndpoint(redis, query);
+    if (response != null) {
+      await redis.set(key, JSON.stringify(response), "EX", cacheTime);
+    }
+    return response;
+  }
+}
+
+async function fetchEndpoint(redis: RedisInstance, query: string) {
+  if (await redis.get("BTTV.RATE_LIMIT")) {
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+  } else {
+    await redis.set("BTTV.RATE_LIMIT", "1", "EX", 1);
+  }
+
+  const requestHeaders = new Headers();
+  requestHeaders.append("Content-Type", "application/json");
+  requestHeaders.append("User-Agent", "toffee-web/indev");
+
+  const response = await fetch(query, {
+    headers: requestHeaders,
+  });
+
+  const json = await response.json();
+  return json;
+}
+
+async function getGlobalEmotes(redis: RedisInstance) {
+  return await applyCache(
+    redis,
+    "BTTV.GLOBAL_EMOTES",
+    "https://api.betterttv.net/3/cached/emotes/global",
+    3600
+  );
+}
+
+async function getUserByID(redis: RedisInstance, channelID: string) {
+  return await applyCache(
+    redis,
+    `BTTV.CHANNEL_EMOTES.${channelID}`,
+    `https://api.betterttv.net/3/cached/users/twitch/${channelID}`,
+    3600
+  );
+}
+
+export { getGlobalEmotes, getUserByID };
diff --git a/misc/FFZAPI.tsx b/misc/FFZAPI.tsx
new file mode 100644
index 0000000..06cb411
--- /dev/null
+++ b/misc/FFZAPI.tsx
@@ -0,0 +1,58 @@
+import type RedisInstance from "ioredis";
+
+async function applyCache(
+  redis: RedisInstance,
+  key: string,
+  query: string,
+
+  cacheTime: number
+) {
+  if (await redis.get(key)) {
+    return JSON.parse((await redis.get(key)) as string);
+  } else {
+    const response = await fetchEndpoint(redis, query);
+    if (response != null) {
+      await redis.set(key, JSON.stringify(response), "EX", cacheTime);
+    }
+    return response;
+  }
+}
+
+async function fetchEndpoint(redis: RedisInstance, query: string) {
+  if (await redis.get("FFZ.RATE_LIMIT")) {
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+  } else {
+    await redis.set("FFZ.RATE_LIMIT", "1", "EX", 1);
+  }
+
+  const requestHeaders = new Headers();
+  requestHeaders.append("Content-Type", "application/json");
+  requestHeaders.append("User-Agent", "toffee-web/indev");
+
+  const response = await fetch(query, {
+    headers: requestHeaders,
+  });
+
+  const json = await response.json();
+  return json;
+}
+
+async function getGlobalEmotes(redis: RedisInstance) {
+  return await applyCache(
+    redis,
+    "FFZ.GLOBAL_EMOTES",
+    "https://api.frankerfacez.com/v1/set/global",
+    3600
+  );
+}
+
+async function getEmoteSet(redis: RedisInstance, setID: string) {
+  return await applyCache(
+    redis,
+    `FFZ.EMOTE_SET.${setID}`,
+    `https://api.frankerfacez.com/v1/set/${setID}`,
+    3600
+  );
+}
+
+export { getGlobalEmotes, getEmoteSet };
diff --git a/misc/TwitchAPI.tsx b/misc/TwitchAPI.tsx
new file mode 100644
index 0000000..ab16d86
--- /dev/null
+++ b/misc/TwitchAPI.tsx
@@ -0,0 +1,94 @@
+import RedisInstance from "ioredis";
+
+async function applyCache(
+  redis: RedisInstance,
+  key: string,
+  query: string,
+  cacheTime: number
+) {
+  if (await redis.get(key)) {
+    return JSON.parse((await redis.get(key)) as string);
+  } else {
+    const response = await fetchEndpoint(redis, query);
+    if (response != null) {
+      await redis.set(key, JSON.stringify(response), "EX", cacheTime);
+    }
+    return response;
+  }
+}
+
+async function authTwitch(redis: RedisInstance) {
+  let auth = await redis.get("TWITCH.AUTH");
+  if (auth) {
+    return auth;
+  } else {
+    const response = await fetch(
+      `https://id.twitch.tv/oauth2/token?client_id=${process.env.TWITCH_CLIENT_ID}&client_secret=${process.env.TWITCH_SECRET}&grant_type=client_credentials`,
+      {
+        method: "POST",
+      }
+    );
+    const json = await response.json();
+    await redis.set("TWITCH.OAUTH", json.access_token, "EX", json.expires_in);
+
+    return json.access_token;
+  }
+}
+
+async function fetchEndpoint(redis: RedisInstance, query: string) {
+  if (await redis.get("TWITCH.RATE_LIMIT")) {
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+  } else {
+    await redis.set("TWITCH.RATE_LIMIT", "1", "EX", 1);
+  }
+
+  const auth = await authTwitch(redis);
+
+  const requestHeaders = new Headers();
+  requestHeaders.append("Client-ID", process.env.TWITCH_CLIENT_ID ?? "");
+  requestHeaders.append("Authorization", `Bearer ${auth}`);
+
+  const response = await fetch(query, {
+    headers: requestHeaders,
+  });
+  const json = await response.json();
+  return json;
+}
+
+async function getUserByName(redis: RedisInstance, username: string) {
+  return await applyCache(
+    redis,
+    "TWITCH.USER_" + username,
+    `https://api.twitch.tv/helix/users?login=${username}`,
+    600
+  );
+}
+
+async function getUserByID(redis: RedisInstance, userID: string) {
+  return await applyCache(
+    redis,
+    "TWITCH.USER_" + userID,
+    `https://api.twitch.tv/helix/users?id=${userID}`,
+    600
+  );
+}
+
+async function getGlobalEmotes(redis: RedisInstance) {
+  return await applyCache(
+    redis,
+    "TWITCH.GLOBAL_EMOTES",
+    `https://api.twitch.tv/helix/chat/emotes/global`,
+    3600
+  );
+}
+
+async function getChannelEmotes(redis: RedisInstance, channelID: string) {
+  return await applyCache(
+    redis,
+    "TWITCH.CHANNEL_EMOTES_" + channelID,
+    `https://api.twitch.tv/helix/chat/emotes?broadcaster_id=${channelID}`,
+    600
+  );
+}
+
+export { getUserByName, getUserByID, getGlobalEmotes, getChannelEmotes };
diff --git a/misc/redis.ts b/misc/redis.ts
new file mode 100644
index 0000000..d118502
--- /dev/null
+++ b/misc/redis.ts
@@ -0,0 +1,55 @@
+// https://makerkit.dev/blog/tutorials/nextjs-redis (I got tired of having redis issues)
+import Redis, { RedisOptions } from "ioredis";
+
+const configuration = {
+  redis: {
+    host: process.env.REDIS_HOST ?? "localhost",
+    port: parseInt(process.env.REDIS_PORT ?? "6379"),
+    password: process.env.REDIS_PASSWORD ?? null,
+  },
+};
+
+function getRedisConfiguration(): {
+  port: number;
+  host: string;
+  password: string | null;
+} {
+  return configuration.redis;
+}
+
+export function createRedisInstance(config = getRedisConfiguration()) {
+  try {
+    const options: RedisOptions = {
+      host: config.host,
+      lazyConnect: true,
+      showFriendlyErrorStack: true,
+      enableAutoPipelining: true,
+      maxRetriesPerRequest: 0,
+      retryStrategy: (times: number) => {
+        if (times > 3) {
+          throw new Error(`[Redis] Could not connect after ${times} attempts`);
+        }
+
+        return Math.min(times * 200, 1000);
+      },
+    };
+
+    if (config.port) {
+      options.port = config.port;
+    }
+
+    if (config.password) {
+      options.password = config.password;
+    }
+
+    const redis = new Redis(options);
+
+    redis.on("error", (error: unknown) => {
+      console.warn("[Redis] Error connecting", error);
+    });
+
+    return redis;
+  } catch (e) {
+    throw new Error(`[Redis] Could not create a Redis instance`);
+  }
+}
diff --git a/next.config.js b/next.config.js
index bd38de3..6f65b5a 100644
--- a/next.config.js
+++ b/next.config.js
@@ -3,7 +3,13 @@ const nextConfig = {
   reactStrictMode: true,
   swcMinify: true,
   images: {
-    domains: ["cdn.discordapp.com", "static-cdn.jtvnw.net", "cdn.7tv.app"],
+    domains: [
+      "cdn.discordapp.com",
+      "static-cdn.jtvnw.net",
+      "cdn.7tv.app",
+      "cdn.betterttv.net",
+      "cdn.frankerfacez.com",
+    ],
   },
 };
 
diff --git a/package-lock.json b/package-lock.json
index 841dc3b..0468ee6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
         "eslint": "8.28.0",
         "eslint-config-next": "13.0.4",
         "framer-motion": "^7.6.19",
-        "ioredis": "^4.28.5",
+        "ioredis": "^5.2.5",
         "next": "13.0.4",
         "react": "18.2.0",
         "react-dom": "18.2.0",
@@ -122,6 +122,11 @@
       "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
     },
+    "node_modules/@ioredis/commands": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
+      "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
+    },
     "node_modules/@motionone/animation": {
       "version": "10.15.1",
       "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz",
@@ -1342,9 +1347,9 @@
       }
     },
     "node_modules/denque": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
-      "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
       "engines": {
         "node": ">=0.10"
       }
@@ -2515,38 +2520,28 @@
       }
     },
     "node_modules/ioredis": {
-      "version": "4.28.5",
-      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
-      "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
+      "version": "5.2.5",
+      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.5.tgz",
+      "integrity": "sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==",
       "dependencies": {
+        "@ioredis/commands": "^1.1.1",
         "cluster-key-slot": "^1.1.0",
-        "debug": "^4.3.1",
-        "denque": "^1.1.0",
+        "debug": "^4.3.4",
+        "denque": "^2.0.1",
         "lodash.defaults": "^4.2.0",
-        "lodash.flatten": "^4.4.0",
         "lodash.isarguments": "^3.1.0",
-        "p-map": "^2.1.0",
-        "redis-commands": "1.7.0",
         "redis-errors": "^1.2.0",
         "redis-parser": "^3.0.0",
         "standard-as-callback": "^2.1.0"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=12.22.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/ioredis"
       }
     },
-    "node_modules/ioredis/node_modules/p-map": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
-      "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/is-arrayish": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -2841,9 +2836,9 @@
       "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
     },
     "node_modules/json5": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
-      "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
       "dependencies": {
         "minimist": "^1.2.0"
       },
@@ -3050,11 +3045,6 @@
       "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
       "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
     },
-    "node_modules/lodash.flatten": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
-      "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
-    },
     "node_modules/lodash.isarguments": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -4025,11 +4015,6 @@
         "node": ">=8.10.0"
       }
     },
-    "node_modules/redis-commands": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
-      "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
-    },
     "node_modules/redis-errors": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
@@ -5075,6 +5060,11 @@
       "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
     },
+    "@ioredis/commands": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
+      "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
+    },
     "@motionone/animation": {
       "version": "10.15.1",
       "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz",
@@ -5868,9 +5858,9 @@
       "dev": true
     },
     "denque": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
-      "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
     },
     "detect-libc": {
       "version": "2.0.1",
@@ -6728,28 +6718,19 @@
       }
     },
     "ioredis": {
-      "version": "4.28.5",
-      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
-      "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
+      "version": "5.2.5",
+      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.5.tgz",
+      "integrity": "sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==",
       "requires": {
+        "@ioredis/commands": "^1.1.1",
         "cluster-key-slot": "^1.1.0",
-        "debug": "^4.3.1",
-        "denque": "^1.1.0",
+        "debug": "^4.3.4",
+        "denque": "^2.0.1",
         "lodash.defaults": "^4.2.0",
-        "lodash.flatten": "^4.4.0",
         "lodash.isarguments": "^3.1.0",
-        "p-map": "^2.1.0",
-        "redis-commands": "1.7.0",
         "redis-errors": "^1.2.0",
         "redis-parser": "^3.0.0",
         "standard-as-callback": "^2.1.0"
-      },
-      "dependencies": {
-        "p-map": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
-          "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="
-        }
       }
     },
     "is-arrayish": {
@@ -6940,9 +6921,9 @@
       "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
     },
     "json5": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
-      "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
       "requires": {
         "minimist": "^1.2.0"
       }
@@ -7094,11 +7075,6 @@
       "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
       "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
     },
-    "lodash.flatten": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
-      "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
-    },
     "lodash.isarguments": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -7736,11 +7712,6 @@
         "picomatch": "^2.2.1"
       }
     },
-    "redis-commands": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
-      "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
-    },
     "redis-errors": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
diff --git a/package.json b/package.json
index 51afc68..dab8fe0 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
     "eslint": "8.28.0",
     "eslint-config-next": "13.0.4",
     "framer-motion": "^7.6.19",
-    "ioredis": "^4.28.5",
+    "ioredis": "^5.2.5",
     "next": "13.0.4",
     "react": "18.2.0",
     "react-dom": "18.2.0",
diff --git a/pages/api/7tv/emotes.ts b/pages/api/7tv/emotes.ts
index ab4694e..4718558 100644
--- a/pages/api/7tv/emotes.ts
+++ b/pages/api/7tv/emotes.ts
@@ -1,4 +1,5 @@
 import type { NextApiRequest, NextApiResponse } from "next";
+import { createRedisInstance } from "../../../misc/redis";
 import { getChannelEmotes, getGlobalEmotes } from "../../../misc/7TVAPI";
 
 type Data = {
@@ -9,10 +10,19 @@ export default async function handler(
   req: NextApiRequest,
   res: NextApiResponse<Data>
 ) {
-  const channel = req.query.c
-    ? await getChannelEmotes(req.query.c as string)
-    : undefined;
-  const global = await getGlobalEmotes();
+  const redis = createRedisInstance();
 
-  res.status(200).json({ channel, global });
+  try {
+    const channel = req.query.c
+      ? await getChannelEmotes(redis, req.query.c as string)
+      : undefined;
+    const global = await getGlobalEmotes(redis);
+    redis.quit();
+    res.status(200).json({ channel, global });
+  } catch (e) {
+    console.log(e);
+    res
+      .status(500)
+      .json({ error: { message: "7TV or internal API is down", code: 10000 } });
+  }
 }
diff --git a/pages/api/bttv/emotes.ts b/pages/api/bttv/emotes.ts
new file mode 100644
index 0000000..fbdf506
--- /dev/null
+++ b/pages/api/bttv/emotes.ts
@@ -0,0 +1,30 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { createRedisInstance } from "../../../misc/redis";
+import { getUserByID, getGlobalEmotes } from "../../../misc/BTTVAPI";
+
+type Data = {
+  [key: string]: any;
+};
+
+export default async function handler(
+  req: NextApiRequest,
+  res: NextApiResponse<Data>
+) {
+  const redis = createRedisInstance();
+
+  try {
+    const channel = req.query.c
+      ? (await getUserByID(redis, req.query.c as string)).channelEmotes
+      : undefined;
+    const global = await getGlobalEmotes(redis);
+    redis.quit();
+    res.status(200).json({ channel, global });
+  } catch (e) {
+    console.log(e);
+    res
+      .status(500)
+      .json({
+        error: { message: "BTTV or internal API is down", code: 10200 },
+      });
+  }
+}
diff --git a/pages/api/fakePrices.ts b/pages/api/fakePrices.ts
new file mode 100644
index 0000000..85fe5e9
--- /dev/null
+++ b/pages/api/fakePrices.ts
@@ -0,0 +1,804 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+
+type Data = {
+  [key: string]: any;
+};
+
+export default async function handler(
+  req: NextApiRequest,
+  res: NextApiResponse<Data>
+) {
+  const emote = req.query.emote ? (req.query.emote as string) : undefined;
+  if (!emote) {
+    res.status(200).json({ data: fakePrices });
+    return;
+  }
+  res.status(200).json({ data: fakePrices[emote] });
+}
+
+const fakePrices: { [key: string]: number } = {
+  "3Head": 521,
+  "4Health": 801,
+  ":3": 104,
+  ":(": 799,
+  AAAA: 717,
+  AAAAUUUUUUGHHHHHHH: 342,
+  AINTNAURWAY: 195,
+  AREYOUAFEMBOY: 135,
+  AREYOUAGIRL: 494,
+  AYAYAjam: 907,
+  Adge: 646,
+  AlienPls3: 460,
+  Amonge: 608,
+  AngelThump: 195,
+  AnnyLebronJam: 967,
+  Aware: 571,
+  BAND: 472,
+  BASED: 794,
+  BEGGING: 365,
+  BLUBBERS: 211,
+  BLUBBERSWTF: 713,
+  BOOBEST: 107,
+  BOTHA: 472,
+  BRUHgers: 536,
+  Baby: 927,
+  Binoculars: 441,
+  Blindge: 426,
+  BocchiPossessed: 545,
+  Bruhgi: 779,
+  CEASE: 708,
+  COCKA: 656,
+  CapybaraStare: 295,
+  CatChest: 267,
+  ChadMeeting: 454,
+  Chatting: 330,
+  ChugU: 661,
+  Clueless: 888,
+  Coldge: 314,
+  Comfi: 187,
+  Copege: 841,
+  Corpa: 536,
+  CumTime: 199,
+  DIESOFAYAYA: 657,
+  DIESOFCRINGE: 320,
+  DONUT: 57,
+  Danki: 864,
+  Deadge: 322,
+  Despairge: 437,
+  Deutschge: 626,
+  EDM: 572,
+  FLASHBANG: 308,
+  FeelsWeakMan: 88,
+  Fishinge: 956,
+  Flushed: 290,
+  FluteTime: 557,
+  GOODONE: 804,
+  GachiPls: 415,
+  Gambage: 876,
+  Gayge: 434,
+  GetOutOfMyHead: 338,
+  GoodGirl: 761,
+  GroupWankge: 540,
+  GuysRefreshChatterinoIUploadedAnotherEmote: 677,
+  HACKERMANS: 85,
+  HUH: 582,
+  HYPERS: 142,
+  Homi: 745,
+  HopOnBlueArchive: 386,
+  IDC: 277,
+  ILOST: 894,
+  JIGACHAD: 865,
+  Jamgie: 156,
+  Jammies: 308,
+  Joel: 586,
+  KEKW: 356,
+  KKool: 821,
+  KoroneOkite: 90,
+  LETHIMCOOK: 940,
+  LICKA: 866,
+  LUBBERS: 808,
+  LULE: 244,
+  Lagging: 314,
+  LickAnnyThighs: 66,
+  Life: 894,
+  LoveForever: 21,
+  MODS: 180,
+  MYHEART: 122,
+  MaN: 181,
+  MadgeJuice: 926,
+  MadgeTime: 481,
+  Madgeclap: 534,
+  Malding: 940,
+  Maxwell: 656,
+  MyHonestReaction: 314,
+  NAUR: 93,
+  NGOthis: 454,
+  NOBOOBS: 855,
+  NODDERS: 836,
+  NOTED: 293,
+  Naruga: 572,
+  Naruge: 872,
+  Nerdge: 46,
+  NessieTwerk: 666,
+  OFFLINECHAT: 780,
+  OMEGA: 934,
+  OMG: 631,
+  "OhTheMiseryEverybodyWantsToBeMyEnemySpareTheSympathyEverybodyWantsToBeMyEnemy-y-y-y-y": 798,
+  Okayeg: 485,
+  OkaygeBusiness: 493,
+  Oldge: 763,
+  OrangexddRun: 96,
+  PEPELEPSY: 122,
+  PLEASE: 607,
+  PagMan: 775,
+  PantsGrab: 503,
+  Party: 181,
+  PeepoKittyHug: 360,
+  PensiveWobble: 843,
+  PepeClown: 384,
+  PepeNPC: 131,
+  Pepepains: 105,
+  PepoG: 987,
+  PogOanny: 14,
+  PogTasty: 211,
+  PogU: 245,
+  Pogpega: 902,
+  PokiShare: 274,
+  Poorge: 789,
+  PoroRoast: 199,
+  Programming: 161,
+  RAGEY: 322,
+  RAVE: 67,
+  RESETTING: 749,
+  RIDING: 364,
+  RIPBOZO: 638,
+  Ratge: 981,
+  SCHIZO: 457,
+  SHITUP: 643,
+  SNIFFA: 220,
+  SPEED: 956,
+  SadCat: 747,
+  Sadeg: 313,
+  SadgeCry: 869,
+  SaguiPls: 363,
+  Siti: 396,
+  Smadge: 222,
+  SmugCat: 447,
+  SnowTime: 545,
+  SoCute: 317,
+  Spoopy: 281,
+  Suske: 642,
+  THESE: 401,
+  THIS: 311,
+  Tasty: 530,
+  ThisStream: 497,
+  TouchGrass: 608,
+  TurtleRush: 608,
+  UOHHH: 426,
+  VIBE: 939,
+  VIBEOFF: 36,
+  VaN: 479,
+  VeryBased: 456,
+  ViolinTime: 406,
+  WHAT: 941,
+  WHOMEGALUL: 804,
+  WakuWaku: 221,
+  WeebRun: 582,
+  WhoAsked: 459,
+  Wickedgi: 870,
+  Wigglecat: 649,
+  Wokege: 377,
+  YourMom: 939,
+  amongnnE: 142,
+  annE: 552,
+  annie: 292,
+  annyBlob: 471,
+  annyBop: 749,
+  annyCD: 673,
+  annyCucumber: 689,
+  annyDespair: 736,
+  annyExcitedHug: 926,
+  annyGAMBA: 885,
+  annyGasm: 49,
+  annyGlare: 462,
+  annyHappy: 960,
+  annyHug: 979,
+  annyJam: 544,
+  annyLava: 288,
+  annyNODDERS: 730,
+  annyNOW: 68,
+  annyOffline: 241,
+  annyPadoru: 453,
+  annyPag: 377,
+  annyPoggies: 729,
+  annyPooPoo: 295,
+  annySCHIZO: 766,
+  annySmile: 467,
+  annySussy: 695,
+  annySwipe: 992,
+  annyTalk: 951,
+  annyThighs: 458,
+  annyVibe: 914,
+  annykiss: 308,
+  annysilly: 996,
+  annystare: 34,
+  annytfCum: 27,
+  anyaPls: 322,
+  anyatf: 58,
+  baseg: 551,
+  borpaSpin: 793,
+  burh: 716,
+  catKISS: 88,
+  catRAVE: 912,
+  catbaby: 673,
+  chatlookwhatannytaughtme: 515,
+  danse: 213,
+  deadass: 815,
+  degen: 551,
+  dejj: 843,
+  donowall: 660,
+  dvaAss: 871,
+  elmoFire: 117,
+  ewLeague: 240,
+  ewOverwatch: 397,
+  forsen: 482,
+  forsenGa: 327,
+  forsenGaPick: 149,
+  forsenLaughingAtYou: 292,
+  frenn: 575,
+  golive: 103,
+  guraFukkireta: 767,
+  guraWiggle: 132,
+  guraWink: 891,
+  happ: 525,
+  heCrazy: 437,
+  hiThere: 54,
+  hmmMeeting: 749,
+  iAsked: 203,
+  jupijej: 207,
+  kiss0: 738,
+  kok: 313,
+  koklick: 990,
+  koroneWink: 824,
+  lebronJAM: 890,
+  liccanny: 205,
+  lobaPls: 436,
+  majj: 813,
+  meow: 757,
+  monkaE: 376,
+  monkaLaugh: 335,
+  nekoNya: 948,
+  nessieWalk: 672,
+  nise: 658,
+  nyanPls: 330,
+  pL: 38,
+  paapoHappy: 126,
+  peepoCat: 673,
+  peepoClap: 772,
+  peepoFAT: 408,
+  peepoFeet: 909,
+  peepoFinger: 185,
+  peepoFlower: 569,
+  peepoFlute: 361,
+  peepoHeadbang: 615,
+  peepoHigh: 125,
+  peepoLeaveToAnny: 711,
+  peepoPag: 101,
+  peepoPopcorn: 234,
+  peepoRant: 628,
+  peepoRiot: 874,
+  peepoSnow: 90,
+  peepoStuck: 188,
+  peepoTalk: 452,
+  pepeKneel: 87,
+  pepePoint: 641,
+  pepegaJAMMER: 603,
+  plink: 892,
+  poggcrazy: 319,
+  pogs: 829,
+  poroPls: 87,
+  ppHop: 589,
+  ppHopper: 843,
+  ppPoof: 188,
+  sadWankge: 300,
+  sajj: 44,
+  sitt: 450,
+  soncic: 483,
+  sonic: 464,
+  squirrelJAM: 431,
+  toffee1: 591,
+  toffee2: 51,
+  toffee3: 3,
+  toffeeBlankies: 249,
+  toffeeBop: 801,
+  toffeeChat: 405,
+  toffeeClap: 552,
+  toffeeComfy: 383,
+  toffeeConfused: 548,
+  toffeeDinkDonk: 782,
+  toffeePat: 235,
+  toffeeTap: 815,
+  vp: 113,
+  wideVIBE: 972,
+  wideannyBop: 866,
+  widepeepoHappy: 969,
+  widepeepoMASTURBATION77769420GANGSHITNOMOREFORTNITE19DOLLERFORTNITECARD: 846,
+  xQchatting: 196,
+  xdd666: 700,
+  xddfdhjsd0f76ds5r26FDSFHD88hjdbsa67vr7xlLLhdsgfcxz632nkDFSGATVMVLN8CXFJJMVMMMM111111111111111111111: 866,
+  xdding: 735,
+  xqcCoomer: 397,
+  xqcGoofy: 360,
+  xqcRecord: 773,
+  xqcTwerk: 531,
+  yayAnny: 437,
+  yoshiJAM: 137,
+  zSpooky: 350,
+  zyzzBass: 680,
+  // bttv
+  ":tf:": 325,
+  CiGrip: 697,
+  DatSauce: 903,
+  ForeverAlone: 663,
+  GabeN: 497,
+  HailHelix: 447,
+  ShoopDaWhoop: 502,
+  "M&Mjc": 196,
+  bttvNice: 558,
+  TwaT: 176,
+  WatChuSay: 947,
+  tehPoleCat: 611,
+  TaxiBro: 527,
+  BroBalt: 384,
+  CandianRage: 431,
+  "D:": 192,
+  VisLaud: 966,
+  KaRappa: 247,
+  FishMoley: 276,
+  Hhhehehe: 945,
+  KKona: 596,
+  PoleDoge: 485,
+  sosGame: 473,
+  CruW: 591,
+  RarePepe: 866,
+  haHAA: 979,
+  FeelsBirthdayMan: 536,
+  RonSmug: 776,
+  KappaCool: 777,
+  FeelsBadMan: 483,
+  bUrself: 934,
+  ConcernDoge: 520,
+  FeelsGoodMan: 202,
+  FireSpeed: 82,
+  NaM: 412,
+  SourPls: 231,
+  FeelsSnowMan: 639,
+  FeelsSnowyMan: 929,
+  LuL: 603,
+  SoSnowy: 647,
+  SaltyCorn: 486,
+  monkaS: 189,
+  VapeNation: 570,
+  ariW: 926,
+  notsquishY: 438,
+  FeelsAmazingMan: 92,
+  DuckerZ: 417,
+  IceCold: 21,
+  SqShy: 84,
+  Wowee: 533,
+  WubTF: 494,
+  cvR: 913,
+  cvL: 337,
+  cvHazmat: 599,
+  cvMask: 423,
+  DogChamp: 947,
+  annytfBanana: 730,
+  annySaur: 236,
+  annytfBlink: 108,
+  AnnySilly: 469,
+  annyBlankies: 747,
+  annyHop: 861,
+  annyHopper: 941,
+  annyTeaTime: 398,
+  annyD: 530,
+  annyDVibe: 907,
+  annyDHyper: 451,
+  annyDFast: 117,
+  KissaVei: 158,
+  Annie: 597,
+  annyPls: 91,
+  // ffz
+  monkaW: 969,
+  "5Head": 750,
+  Prayge: 434,
+  PauseChamp: 903,
+  Pepega: 817,
+  TWINGO: 256,
+  PepeHands: 190,
+  monkaTOS: 146,
+  monkaHmm: 549,
+  Dolan: 536,
+  SmileW: 166,
+  ABABABABA: 95,
+  peepoBlanket: 798,
+  pikachuS: 715,
+  AYAYAHyper: 164,
+  YEP: 371,
+  widepeepoBlanket: 100,
+  HandsUp: 684,
+  peepoSad: 167,
+  HyperKorone: 456,
+  AYAYA: 503,
+  forsenCD: 344,
+  Hahaa: 291,
+  LULW: 463,
+  WICKED: 133,
+  EZY: 494,
+  OkayChamp: 950,
+  PepegaPig: 217,
+  POGGIES: 487,
+  peepoWTF: 734,
+  ConfusedCat: 508,
+  PainPeko: 433,
+  KKrikey: 235,
+  COPIUM: 582,
+  Madge: 595,
+  Catge: 811,
+  stopbeingMean: 522,
+  NOPE: 262,
+  OMEGALUL: 648,
+  AYAYAY: 725,
+  PogO: 548,
+  Sadge: 41,
+  PepegaPhone: 800,
+  Widega: 543,
+  ZrehplaR: 23,
+  YooHoo: 617,
+  ManChicken: 588,
+  BeanieHipster: 480,
+  CatBag: 253,
+  ZreknarF: 143,
+  LilZ: 952,
+  ZliL: 662,
+  LaterSooner: 375,
+  BORT: 738,
+  OBOY: 942,
+  OiMinna: 354,
+  AndKnuckles: 828,
+  // twitch
+  annytfRaid: 493,
+  annytfNote: 893,
+  annytfPout: 971,
+  annytfAyaya: 180,
+  annytfPika: 328,
+  annytfPrime: 830,
+  annytfLewd: 314,
+  annytfRave: 615,
+  annytfLurk: 898,
+  annytfPog: 958,
+  annytfD: 759,
+  annytfCry: 399,
+  annytfHeart: 472,
+  annytfSad: 390,
+  annytfMelt: 157,
+  annytfWICKED: 94,
+  annytfCheer: 780,
+  annytfREEE: 272,
+  annytfLUL: 122,
+  annytfScuffed: 318,
+  annytfAngy: 677,
+  annytfHug: 826,
+  annytfCool: 121,
+  annytfPain: 16,
+  annytfBonk: 949,
+  annytfKnuckles: 182,
+  annytfSigh: 220,
+  annytfWoah: 582,
+  annytfBite: 580,
+  annytfSilly: 837,
+  annytfWow: 381,
+  annytfPray: 438,
+  annytfPats: 410,
+  annytfGasm: 822,
+  annytfSit: 39,
+  annytfFlower: 137,
+  annytfLeave: 819,
+  annytfGamba: 847,
+  annytfLuv: 483,
+  annytfHarucchiHug: 142,
+  annytfAnnE: 324,
+  annytfDinkDonk: 380,
+  annytfAhriluv: 232,
+  annytfW: 101,
+  annytfWoke: 145,
+  annytfBedge: 409,
+  annytfBusiness: 142,
+  annytfPeek: 98,
+  annytfDonowall: 581,
+  NewRecord: 836,
+  Awwdible: 794,
+  Lechonk: 567,
+  Getcamped: 378,
+  SUBprise: 580,
+  FallHalp: 95,
+  FallCry: 450,
+  FallWinning: 261,
+  MechaRobot: 984,
+  ImTyping: 993,
+  Shush: 506,
+  MyAvatar: 41,
+  PizzaTime: 330,
+  LaundryBasket: 852,
+  ModLove: 683,
+  PotFriend: 605,
+  Jebasted: 72,
+  PogBones: 761,
+  PoroSad: 357,
+  KEKHeim: 290,
+  CaitlynS: 874,
+  HarleyWink: 244,
+  WhySoSerious: 705,
+  DarkKnight: 101,
+  FamilyMan: 278,
+  RyuChamp: 814,
+  HungryPaimon: 920,
+  TransgenderPride: 409,
+  PansexualPride: 707,
+  NonbinaryPride: 866,
+  LesbianPride: 763,
+  IntersexPride: 136,
+  GenderFluidPride: 73,
+  GayPride: 471,
+  BisexualPride: 163,
+  AsexualPride: 533,
+  PogChamp: 898,
+  GlitchNRG: 877,
+  GlitchLit: 45,
+  StinkyGlitch: 697,
+  GlitchCat: 208,
+  FootGoal: 542,
+  FootYellow: 297,
+  FootBall: 679,
+  BlackLivesMatter: 637,
+  ExtraLife: 394,
+  VirtualHug: 452,
+  "R-)": 779,
+  "R)": 242,
+  ";-p": 911,
+  ";p": 168,
+  ";-P": 584,
+  ";P": 571,
+  ":-p": 758,
+  ":p": 709,
+  ":-P": 137,
+  ":P": 602,
+  ";-)": 349,
+  ";)": 745,
+  ":-\\": 30,
+  ":\\": 725,
+  ":-/": 403,
+  ":/": 187,
+  "<3": 51,
+  ":-o": 622,
+  ":o": 384,
+  ":-O": 994,
+  ":O": 629,
+  "8-)": 881,
+  "B-)": 390,
+  "B)": 489,
+  "o.o": 570,
+  o_o: 738,
+  "o.O": 287,
+  o_O: 168,
+  "O.O": 452,
+  O_O: 33,
+  "O.o": 187,
+  O_o: 513,
+  ":-Z": 729,
+  ":Z": 762,
+  ":-z": 658,
+  ":z": 526,
+  ":-|": 978,
+  ":|": 224,
+  ">(": 141,
+  ":-D": 699,
+  ":D": 926,
+  ":-(": 864,
+  ":-)": 141,
+  BOP: 826,
+  SingsNote: 46,
+  SingsMic: 347,
+  TwitchSings: 419,
+  SoonerLater: 187,
+  HolidayTree: 822,
+  HolidaySanta: 340,
+  HolidayPresent: 830,
+  HolidayLog: 251,
+  HolidayCookie: 105,
+  GunRun: 899,
+  PixelBob: 829,
+  FBPenalty: 123,
+  FBChallenge: 645,
+  FBCatch: 769,
+  FBBlock: 284,
+  FBSpiral: 633,
+  FBPass: 365,
+  FBRun: 688,
+  MaxLOL: 17,
+  TwitchRPG: 258,
+  PinkMercy: 369,
+  MercyWing2: 2,
+  MercyWing1: 546,
+  PartyHat: 581,
+  EarthDay: 649,
+  TombRaid: 904,
+  PopCorn: 485,
+  FBtouchdown: 621,
+  TPFufun: 321,
+  TwitchVotes: 858,
+  DarkMode: 90,
+  HSWP: 729,
+  HSCheers: 802,
+  PowerUpL: 88,
+  PowerUpR: 616,
+  LUL: 958,
+  EntropyWins: 639,
+  TPcrunchyroll: 286,
+  TwitchUnity: 349,
+  Squid4: 548,
+  Squid3: 113,
+  Squid2: 768,
+  Squid1: 649,
+  CrreamAwk: 186,
+  CarlSmile: 822,
+  TwitchLit: 125,
+  TehePelo: 124,
+  TearGlove: 354,
+  SabaPing: 94,
+  PunOko: 145,
+  KonCha: 656,
+  Kappu: 597,
+  InuyoFace: 434,
+  BigPhish: 169,
+  BegWan: 621,
+  ThankEgg: 391,
+  MorphinTime: 106,
+  TheIlluminati: 531,
+  TBAngel: 925,
+  MVGame: 873,
+  NinjaGrumpy: 345,
+  PartyTime: 773,
+  RlyTho: 830,
+  UWot: 265,
+  YouDontSay: 744,
+  KAPOW: 757,
+  ItsBoshyTime: 605,
+  CoolStoryBob: 193,
+  TriHard: 121,
+  SuperVinlin: 500,
+  FreakinStinkin: 860,
+  Poooound: 411,
+  CurseLit: 318,
+  BatChest: 642,
+  BrainSlug: 48,
+  PrimeMe: 619,
+  StrawBeary: 813,
+  RaccAttack: 172,
+  UncleNox: 583,
+  WTRuck: 118,
+  TooSpicy: 761,
+  Jebaited: 363,
+  DogFace: 911,
+  BlargNaut: 148,
+  TakeNRG: 27,
+  GivePLZ: 581,
+  imGlitch: 514,
+  pastaThat: 48,
+  copyThis: 426,
+  UnSane: 97,
+  DatSheffy: 289,
+  TheTarFu: 818,
+  PicoMause: 570,
+  TinyFace: 31,
+  DxCat: 538,
+  RuleFive: 903,
+  VoteNay: 113,
+  VoteYea: 223,
+  PJSugar: 11,
+  DoritosChip: 187,
+  OpieOP: 977,
+  FutureMan: 893,
+  ChefFrank: 481,
+  StinkyCheese: 419,
+  NomNom: 162,
+  SmoocherZ: 863,
+  cmonBruh: 93,
+  KappaWealth: 776,
+  MikeHogu: 497,
+  VoHiYo: 646,
+  KomodoHype: 295,
+  SeriousSloth: 379,
+  OSFrog: 807,
+  OhMyDog: 124,
+  KappaClaus: 209,
+  KappaRoss: 298,
+  MingLee: 338,
+  SeemsGood: 89,
+  twitchRaid: 258,
+  bleedPurple: 949,
+  duDudu: 442,
+  riPepperonis: 192,
+  NotLikeThis: 838,
+  DendiFace: 534,
+  CoolCat: 995,
+  KappaPride: 915,
+  ShadyLulu: 372,
+  ArgieB8: 267,
+  CorgiDerp: 511,
+  PraiseIt: 557,
+  TTours: 122,
+  mcaT: 154,
+  NotATK: 388,
+  HeyGuys: 453,
+  Mau5: 421,
+  PRChase: 443,
+  WutFace: 20,
+  BuddhaBar: 622,
+  PermaSmug: 769,
+  panicBasket: 285,
+  BabyRage: 315,
+  HassaanChop: 246,
+  TheThing: 890,
+  EleGiggle: 284,
+  RitzMitz: 671,
+  YouWHY: 796,
+  PipeHype: 343,
+  BrokeBack: 440,
+  ANELE: 156,
+  PanicVis: 865,
+  GrammarKing: 77,
+  PeoplesChamp: 634,
+  SoBayed: 700,
+  BigBrother: 657,
+  Keepo: 800,
+  Kippa: 835,
+  RalpherZ: 322,
+  TF2John: 862,
+  ThunBeast: 408,
+  WholeWheat: 193,
+  DAESuppy: 787,
+  FailFish: 395,
+  HotPokket: 399,
+  ResidentSleeper: 460,
+  FUNgineer: 747,
+  PMSTwin: 830,
+  ShazBotstix: 315,
+  BibleThump: 278,
+  AsianGlow: 461,
+  DBstyle: 968,
+  BloodTrail: 687,
+  OneHand: 801,
+  FrankerZ: 893,
+  SMOrc: 727,
+  ArsonNoSexy: 99,
+  PunchTrees: 762,
+  SSSsss: 800,
+  Kreygasm: 413,
+  KevinTurtle: 111,
+  PJSalt: 115,
+  SwiftRage: 251,
+  DansGame: 46,
+  GingerPower: 762,
+  BCWarrior: 409,
+  MrDestructoid: 811,
+  JonCarnage: 359,
+  Kappa: 40,
+  RedCoat: 789,
+  TheRinger: 669,
+  StoneLightning: 867,
+  OptimizePrime: 654,
+  JKanStyle: 655,
+  ":)": 594,
+};
+
+export { fakePrices };
diff --git a/pages/api/fakeRanking.ts b/pages/api/fakeRanking.ts
deleted file mode 100644
index d7eb206..0000000
--- a/pages/api/fakeRanking.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-import type { NextApiRequest, NextApiResponse } from "next";
-
-type Data = {
-  [key: string]: any;
-};
-
-export default async function handler(
-  req: NextApiRequest,
-  res: NextApiResponse<Data>
-) {
-  const sortBy = req.query.s ? (req.query.s as string) : undefined;
-  const sortAsc = req.query.a ? (req.query.a as string) : undefined;
-
-  let data = fakeData;
-  if (sortBy) {
-    if (sortBy === "netWorth") {
-      data = data.sort((a, b) => b.netWorth - a.netWorth);
-    } else if (sortBy === "dailyChange") {
-      data = data.sort((a, b) => b.dailyChange - a.dailyChange);
-    } else if (sortBy === "dailyChangePercent") {
-      data = data.sort((a, b) => b.dailyChangePercent - a.dailyChangePercent);
-    } else if (sortBy === "shares") {
-      data = data.sort((a, b) => b.shares - a.shares);
-    } else if (sortBy === "points") {
-      data = data.sort((a, b) => b.points - a.points);
-    } else if (sortBy === "name") {
-      data = data.sort((a, b) => a.name.localeCompare(b.name));
-    }
-    if (sortAsc === "true") {
-      // slow but only needed for temporary fake data anyway
-      data = data.reverse();
-    }
-  }
-  // fake loading time
-  await new Promise((resolve) =>
-    setTimeout(resolve, 250 + Math.random() * 1000)
-  );
-  res.status(200).json({ data });
-}
-
-interface fakeDataEntry {
-  id: number;
-  name: string;
-  netWorth: number;
-  points: number;
-  shares: number;
-  dailyChange: number;
-  dailyChangePercent: number;
-}
-
-const fakeData: fakeDataEntry[] = [
-  {
-    id: 4,
-    name: "3zachm",
-    netWorth: 10030, // stocks + points
-    points: 70, /// uninvested points
-    shares: 20,
-    dailyChange: -500,
-    dailyChangePercent: -0.0498504486540378863409770687936,
-  },
-  {
-    id: 1,
-    name: "ModulatingForce",
-    netWorth: 142910,
-    points: 10020,
-    shares: 200,
-    dailyChange: 5420,
-    dailyChangePercent: 0.0379259673920649359736897347981,
-  },
-  {
-    id: 2,
-    name: "notohh",
-    netWorth: 153495392,
-    points: 10020,
-    shares: 2432,
-    dailyChange: 0,
-    dailyChangePercent: 0,
-  },
-  {
-    id: 3,
-    name: "SecondSockSan",
-    netWorth: 153495,
-    points: 15020,
-    shares: 20,
-    dailyChange: -10432,
-    dailyChangePercent: -0.06796312583471774324896576435715,
-  },
-  {
-    id: 0,
-    name: "e__n__t__e",
-    netWorth: 429481824,
-    points: 1002022,
-    shares: 94214,
-    dailyChange: 329444422,
-    dailyChangePercent: 4.2932124926634939999741296760186,
-  },
-  {
-    id: 5,
-    name: "luckytohavefoundyou14252",
-    netWorth: 8024,
-    points: 423,
-    shares: 4,
-    dailyChange: 9,
-    dailyChangePercent: 0.00112163509471585244267198404786,
-  },
-  {
-    id: 6,
-    name: "ZeroxZerich",
-    netWorth: 842190,
-    points: 88542,
-    shares: 532,
-    dailyChange: -10219,
-    dailyChangePercent: -0.01213384153219582279533121979601,
-  },
-  {
-    id: 7,
-    name: "joeeyo",
-    netWorth: 10000000,
-    points: 9999979,
-    shares: 1,
-    dailyChange: 1,
-    dailyChangePercent: 0.0000001,
-  },
-  {
-    id: 8,
-    name: "dd_maru",
-    netWorth: 10328421,
-    points: 328421,
-    shares: 252,
-    dailyChange: 85192,
-    dailyChangePercent: 0.00824830823607984221402284047097,
-  },
-  {
-    id: 9,
-    name: "Goldeneye128",
-    netWorth: 58292,
-    points: 6521,
-    shares: 63,
-    dailyChange: -1942,
-    dailyChangePercent: -0.03331503465312564331297605160228,
-  },
-  {
-    id: 10,
-    name: "lilpastatv",
-    netWorth: 7328919,
-    points: 40,
-    shares: 93,
-    dailyChange: 921821,
-    dailyChangePercent: 0.12577857662228222197571019682439,
-  },
-  {
-    id: 11,
-    name: "domiswitch",
-    netWorth: 43290,
-    points: 5002,
-    shares: 15,
-    dailyChange: 2429,
-    dailyChangePercent: 0.05610995610995610995610995610996,
-  },
-  {
-    id: 12,
-    name: "minosura",
-    netWorth: 904328,
-    points: 32901,
-    shares: 83,
-    dailyChange: 94821,
-    dailyChangePercent: 0.10485244291894091524314186887943,
-  },
-  {
-    id: 13,
-    name: "scienceteam_member",
-    netWorth: 34894,
-    points: 958,
-    shares: 5,
-    dailyChange: -7964,
-    dailyChangePercent: -0.22823408035765461110792686421734,
-  },
-  {
-    id: 14,
-    name: "witchdev",
-    netWorth: 94382912,
-    points: 8532,
-    shares: 329,
-    dailyChange: -421,
-    dailyChangePercent: -0.0000044605531984433792422085896,
-  },
-  {
-    id: 15,
-    name: "justone123879",
-    netWorth: 8889123,
-    points: 86333,
-    shares: 153,
-    dailyChange: 53289,
-    dailyChangePercent: 0.00599485461051669551653183334284,
-  },
-  {
-    id: 16,
-    name: "marcelr_",
-    netWorth: 400329,
-    points: 39291,
-    shares: 52,
-    dailyChange: 1329,
-    dailyChangePercent: 0.00331976948959480827019776234047,
-  },
-  {
-    id: 17,
-    name: "fossabot",
-    netWorth: 20005,
-    points: 0,
-    shares: 1,
-    dailyChange: -31042,
-    dailyChangePercent: -1.5517120719820044988752811797051,
-  },
-];
-
-export type { fakeDataEntry };
diff --git a/pages/api/fakeUsers.ts b/pages/api/fakeUsers.ts
new file mode 100644
index 0000000..052e9dc
--- /dev/null
+++ b/pages/api/fakeUsers.ts
@@ -0,0 +1,856 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { createRedisInstance } from "../../misc/redis";
+import { getUserByName } from "../../misc/TwitchAPI";
+import { fakePrices } from "./fakePrices";
+
+type Data = {
+  [key: string]: any;
+};
+
+export default async function handler(
+  req: NextApiRequest,
+  res: NextApiResponse<Data>
+) {
+  const username = req.query.u ? (req.query.u as string) : undefined;
+  const sortBy = req.query.s ? (req.query.s as string) : undefined;
+  const sortAsc = req.query.a ? (req.query.a as string) : undefined;
+
+  const redis = createRedisInstance();
+
+  let data = fakeData;
+  // calculate all net worths
+  data = data.map((user) => {
+    return {
+      ...user,
+      net_worth:
+        user.points +
+        user.assets.reduce(
+          (a, b) => a + b.count * (fakePrices[b.name] ?? 0),
+          0
+        ),
+    };
+  });
+  // calculate ranking based on net worth
+  data = data.sort((a, b) => (b.net_worth ?? 0) - (a.net_worth ?? 0));
+  data = data.map((user, i) => {
+    return {
+      ...user,
+      rank: i + 1,
+      // calculate total assets held (shares)
+      shares: user.assets.reduce((a, b) => a + b.count, 0),
+      // sort users badges by priority
+      badges: (user.badges ?? []).sort((a, b) => b.priority - a.priority ?? 0),
+      // sort users assets by total value
+      assets: user.assets.sort(
+        (a, b) =>
+          (fakePrices[b.name] ?? 0) * b.count -
+          (fakePrices[a.name] ?? 0) * a.count
+      ),
+    };
+  });
+
+  // if username is specified, only return that user
+  if (username) {
+    // if user does not exist, return error
+    data = data.filter((u) => u.name === username);
+    if (data.length === 0) {
+      res
+        .status(404)
+        .json({ error: { message: "User not found", code: 20000 } });
+      return;
+    }
+    // get twitch data for user
+    let twitchData: { data: { [key: string]: any }[] };
+    try {
+      twitchData = await getUserByName(redis, username);
+    } catch (e) {
+      res.status(500).json({
+        error: { message: "Twitch or internal API is down", code: 10100 },
+      });
+      return;
+    }
+    // if data is empty, user does not exist
+    if (twitchData.data.length === 0) {
+      // temp who cares
+      twitchData.data[0] = {};
+      twitchData.data[0].profile_image_url = "/img/logo.webp";
+    }
+    // add users profile picture url
+    data = data.map((u) => {
+      return {
+        ...u,
+        avatar_url: twitchData.data[0].profile_image_url ?? "",
+      };
+    });
+    res.status(200).json({ data: data[0] });
+    return;
+  }
+  if (sortBy) {
+    if (sortBy === "daily_change") {
+      data = data.sort((a, b) => b.daily_change - a.daily_change);
+    } else if (sortBy === "daily_change_percent") {
+      data = data.sort(
+        (a, b) => b.daily_change_percent - a.daily_change_percent
+      );
+    } else if (sortBy === "shares") {
+      data = data.sort((a, b) => (b.shares ?? 0) - (a.shares ?? 0));
+    } else if (sortBy === "points") {
+      data = data.sort((a, b) => b.points - a.points);
+    } else if (sortBy === "name") {
+      data = data.sort((a, b) => a.name.localeCompare(b.name));
+    }
+    if (sortAsc === "true") {
+      // slow but only needed for temporary fake data anyway
+      data = data.reverse();
+    }
+  }
+  // fake loading time
+  await new Promise((resolve) =>
+    setTimeout(resolve, 250 + Math.random() * 1000)
+  );
+  res.status(200).json({ data: data });
+}
+
+interface asset {
+  name: string;
+  count: number;
+  provider: "7tv" | "bttv" | "ffz" | "ttv";
+}
+interface fakeDataEntry {
+  id: number;
+  name: string;
+  points: number;
+  daily_change: number;
+  daily_change_percent: number;
+  assets: asset[];
+  net_worth?: number;
+  shares?: number;
+  avatar_url?: string;
+  badges?: badge[];
+}
+
+interface badge {
+  name: string;
+  color: string;
+  priority: number;
+}
+
+const adminBadge: badge = {
+  name: "Admin",
+  color: "#CC3333",
+  priority: 99999,
+};
+
+const CEOBadge: badge = {
+  name: "CEO",
+  color: "#F97316",
+  priority: 100000,
+};
+
+const webDevBadge: badge = {
+  name: "Web Dev",
+  color: "#a855f7",
+  priority: 50000,
+};
+
+const botDevBadge: badge = {
+  name: "Bot Dev",
+  color: "#48b2f1",
+  priority: 50001,
+};
+
+const fakeData: fakeDataEntry[] = [
+  {
+    id: 4,
+    name: "3zachm",
+    points: 7420, // uninvested points
+    daily_change: -500,
+    daily_change_percent: -0.0498504486540378863409770687936,
+    assets: [
+      {
+        name: "JIGACHAD",
+        count: 420,
+        provider: "7tv",
+      },
+      {
+        name: "annykiss",
+        count: 8,
+        provider: "7tv",
+      },
+      {
+        name: "HUH",
+        count: 1,
+        provider: "7tv",
+      },
+      {
+        name: "annytfSigh",
+        count: 1,
+        provider: "ttv",
+      },
+      {
+        name: "GabeN",
+        count: 3,
+        provider: "bttv",
+      },
+      {
+        name: "widepeepoBlanket",
+        count: 1,
+        provider: "ffz",
+      },
+      {
+        name: "plink",
+        count: 1,
+        provider: "7tv",
+      },
+    ],
+    badges: [adminBadge, webDevBadge],
+  },
+  {
+    id: 1,
+    name: "ModulatingForce",
+    points: 10020,
+    daily_change: 5420,
+    daily_change_percent: 0.0379259673920649359736897347981,
+    assets: [
+      {
+        name: "OhTheMiseryEverybodyWantsToBeMyEnemySpareTheSympathyEverybodyWantsToBeMyEnemy-y-y-y-y",
+        count: 39,
+        provider: "7tv",
+      },
+      {
+        name: "Clueless",
+        count: 727,
+        provider: "7tv",
+      },
+      {
+        name: "AnnySilly",
+        count: 4,
+        provider: "bttv",
+      },
+      {
+        name: "annytfHeart",
+        count: 98,
+        provider: "ttv",
+      },
+      {
+        name: "Catge",
+        count: 4,
+        provider: "ffz",
+      },
+    ],
+    badges: [adminBadge, botDevBadge],
+  },
+  {
+    id: 2,
+    name: "notohh",
+    points: 10020,
+    daily_change: 0,
+    daily_change_percent: 0,
+    assets: [
+      {
+        name: "YourMom",
+        count: 81,
+        provider: "7tv",
+      },
+      {
+        name: "CumTime",
+        count: 92,
+        provider: "7tv",
+      },
+      {
+        name: "KissaVei",
+        count: 1,
+        provider: "bttv",
+      },
+      {
+        name: "SNIFFA",
+        count: 1219,
+        provider: "7tv",
+      },
+      {
+        name: "FeelsBirthdayMan",
+        count: 1,
+        provider: "ffz",
+      },
+      {
+        name: "annytfRave",
+        count: 5,
+        provider: "ttv",
+      },
+    ],
+    badges: [adminBadge, botDevBadge],
+  },
+  {
+    id: 3,
+    name: "SecondSockSan",
+    points: 15020,
+    daily_change: -10432,
+    daily_change_percent: -0.06796312583471774324896576435715,
+    assets: [
+      {
+        name: "AYAYAjam",
+        count: 46,
+        provider: "7tv",
+      },
+      {
+        name: "GabeN",
+        count: 3,
+        provider: "bttv",
+      },
+      {
+        name: "ThisStream",
+        count: 210,
+        provider: "7tv",
+      },
+      {
+        name: "BAND",
+        count: 91,
+        provider: "7tv",
+      },
+      {
+        name: "annytfMelt",
+        count: 16,
+        provider: "ttv",
+      },
+    ],
+    badges: [CEOBadge, adminBadge],
+  },
+  {
+    id: 0,
+    name: "mzntori",
+    points: 922022,
+    daily_change: 329444422,
+    daily_change_percent: 4.2932124926634939999741296760186,
+    assets: [
+      {
+        name: "peepoSnow",
+        count: 72,
+        provider: "7tv",
+      },
+      {
+        name: "annyHop",
+        count: 61,
+        provider: "bttv",
+      },
+      {
+        name: "annyExcitedHug",
+        count: 26,
+        provider: "7tv",
+      },
+      {
+        name: "AAAA",
+        count: 65,
+        provider: "7tv",
+      },
+      {
+        name: "peepoWTF",
+        count: 60,
+        provider: "ffz",
+      },
+      {
+        name: "annytfAngy",
+        count: 90,
+        provider: "ttv",
+      },
+    ],
+    badges: [adminBadge, botDevBadge],
+  },
+  {
+    id: 5,
+    name: "luckytohavefoundyou14252",
+    points: 423,
+    daily_change: 9,
+    daily_change_percent: 0.00112163509471585244267198404786,
+    assets: [
+      {
+        name: "HACKERMANS",
+        count: 59,
+        provider: "7tv",
+      },
+      {
+        name: "THIS",
+        count: 70,
+        provider: "7tv",
+      },
+      {
+        name: "lebronJAM",
+        count: 66,
+        provider: "7tv",
+      },
+    ],
+  },
+  {
+    id: 6,
+    name: "ZeroxZerich",
+    points: 88542,
+    daily_change: -10219,
+    daily_change_percent: -0.01213384153219582279533121979601,
+    assets: [
+      {
+        name: "WeebRun",
+        count: 10,
+        provider: "7tv",
+      },
+      {
+        name: "annySaur",
+        count: 7,
+        provider: "bttv",
+      },
+      {
+        name: "BAND",
+        count: 49,
+        provider: "7tv",
+      },
+      {
+        name: "SNIFFA",
+        count: 78,
+        provider: "7tv",
+      },
+      {
+        name: "PepegaPhone",
+        count: 142,
+        provider: "ffz",
+      },
+      {
+        name: "annytfHug",
+        count: 19,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 7,
+    name: "joeeyo",
+    points: 99979,
+    daily_change: 1,
+    daily_change_percent: 0.0000001,
+    assets: [
+      {
+        name: "Siti",
+        count: 32,
+        provider: "7tv",
+      },
+      {
+        name: "annytfLUL",
+        count: 9,
+        provider: "ttv",
+      },
+      {
+        name: "peepoSnow",
+        count: 37,
+        provider: "7tv",
+      },
+      {
+        name: "MadgeJuice",
+        count: 70,
+        provider: "7tv",
+      },
+      {
+        name: "annyBlankies",
+        count: 88,
+        provider: "bttv",
+      },
+      {
+        name: "TWINGO",
+        count: 98,
+        provider: "ffz",
+      },
+    ],
+  },
+  {
+    id: 8,
+    name: "dd_maru",
+    points: 208421,
+    daily_change: 85192,
+    daily_change_percent: 0.00824830823607984221402284047097,
+    assets: [
+      {
+        name: "BocchiPossessed",
+        count: 56,
+        provider: "7tv",
+      },
+      {
+        name: "toffeeConfused",
+        count: 64,
+        provider: "7tv",
+      },
+      {
+        name: "annytfBanana",
+        count: 15,
+        provider: "bttv",
+      },
+      {
+        name: "ewLeague",
+        count: 64,
+        provider: "7tv",
+      },
+      {
+        name: "annytfPain",
+        count: 37,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 9,
+    name: "Goldeneye128",
+    points: 6521,
+    daily_change: -1942,
+    daily_change_percent: -0.03331503465312564331297605160228,
+    assets: [
+      {
+        name: "PagMan",
+        count: 52,
+        provider: "7tv",
+      },
+      {
+        name: "CapybaraStare",
+        count: 47,
+        provider: "7tv",
+      },
+      {
+        name: "GabeN",
+        count: 52,
+        provider: "bttv",
+      },
+      {
+        name: "GroupWankge",
+        count: 38,
+        provider: "7tv",
+      },
+      {
+        name: "annyCucumber",
+        count: 90,
+        provider: "7tv",
+      },
+      {
+        name: "annytfKnuckles",
+        count: 2,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 10,
+    name: "lilpastatv",
+    points: 40,
+    daily_change: 921821,
+    daily_change_percent: 0.12577857662228222197571019682439,
+    assets: [
+      {
+        name: "Wigglecat",
+        count: 205,
+        provider: "7tv",
+      },
+      {
+        name: "guraWink",
+        count: 5,
+        provider: "7tv",
+      },
+      {
+        name: "annyPls",
+        count: 5,
+        provider: "bttv",
+      },
+      {
+        name: "golive",
+        count: 46,
+        provider: "7tv",
+      },
+      {
+        name: "COPIUM",
+        count: 82,
+        provider: "ffz",
+      },
+      {
+        name: "annytfCheer",
+        count: 54,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 11,
+    name: "domiswitch",
+    points: 5002,
+    daily_change: 2429,
+    daily_change_percent: 0.05610995610995610995610995610996,
+    assets: [
+      {
+        name: "peepoFlute",
+        count: 81,
+        provider: "7tv",
+      },
+      {
+        name: "WhoAsked",
+        count: 44,
+        provider: "7tv",
+      },
+      {
+        name: "pL",
+        count: 24,
+        provider: "7tv",
+      },
+      {
+        name: "peepoSnow",
+        count: 13,
+        provider: "7tv",
+      },
+      {
+        name: "annytfBlink",
+        count: 10,
+        provider: "bttv",
+      },
+      {
+        name: "annytfBonk",
+        count: 77,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 12,
+    name: "minosura",
+    points: 32901,
+    daily_change: 94821,
+    daily_change_percent: 0.10485244291894091524314186887943,
+    assets: [
+      {
+        name: "Okayeg",
+        count: 100,
+        provider: "7tv",
+      },
+      {
+        name: "burh",
+        count: 100,
+        provider: "7tv",
+      },
+      {
+        name: "annyHop",
+        count: 16,
+        provider: "bttv",
+      },
+      {
+        name: "AndKnuckles",
+        count: 17,
+        provider: "ffz",
+      },
+      {
+        name: "yoshiJAM",
+        count: 67,
+        provider: "7tv",
+      },
+      {
+        name: "WhoAsked",
+        count: 59,
+        provider: "7tv",
+      },
+      {
+        name: "annytfSit",
+        count: 53,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 13,
+    name: "scienceteam_member",
+    points: 958,
+    daily_change: -7964,
+    daily_change_percent: -0.22823408035765461110792686421734,
+    assets: [
+      {
+        name: "LULE",
+        count: 43,
+        provider: "7tv",
+      },
+      {
+        name: "Madgeclap",
+        count: 82,
+        provider: "7tv",
+      },
+      {
+        name: "annyDFast",
+        count: 22,
+        provider: "bttv",
+      },
+      {
+        name: "PeepoKittyHug",
+        count: 7,
+        provider: "7tv",
+      },
+    ],
+  },
+  {
+    id: 14,
+    name: "witchdev",
+    points: 8532,
+    daily_change: -421,
+    daily_change_percent: -0.0000044605531984433792422085896,
+    assets: [
+      {
+        name: "SNIFFA",
+        count: 76,
+        provider: "7tv",
+      },
+      {
+        name: "annyCD",
+        count: 62,
+        provider: "7tv",
+      },
+      {
+        name: "annyBlankies",
+        count: 74,
+        provider: "bttv",
+      },
+      {
+        name: "anyatf",
+        count: 24,
+        provider: "7tv",
+      },
+      {
+        name: "annytfGamba",
+        count: 32,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 15,
+    name: "justone123879",
+    points: 86333,
+    daily_change: 53289,
+    daily_change_percent: 0.00599485461051669551653183334284,
+    assets: [
+      {
+        name: "Homi",
+        count: 9,
+        provider: "7tv",
+      },
+      {
+        name: "wideVIBE",
+        count: 61,
+        provider: "7tv",
+      },
+      {
+        name: "Annie",
+        count: 24,
+        provider: "bttv",
+      },
+      {
+        name: "Lagging",
+        count: 92,
+        provider: "7tv",
+      },
+      {
+        name: "annytfFlower",
+        count: 33,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 16,
+    name: "marcelr_",
+    points: 39291,
+    daily_change: 1329,
+    daily_change_percent: 0.00331976948959480827019776234047,
+    assets: [
+      {
+        name: "peepoStuck",
+        count: 91,
+        provider: "7tv",
+      },
+      {
+        name: "PokiShare",
+        count: 13,
+        provider: "7tv",
+      },
+      {
+        name: "VeryBased",
+        count: 7,
+        provider: "7tv",
+      },
+      {
+        name: "annyHopper",
+        count: 24,
+        provider: "bttv",
+      },
+      {
+        name: "annytfFlower",
+        count: 79,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 17,
+    name: "fossabot",
+    points: 0,
+    daily_change: -31042,
+    daily_change_percent: -1.5517120719820044988752811797051,
+    assets: [
+      {
+        name: "catbaby",
+        count: 41,
+        provider: "7tv",
+      },
+      {
+        name: "peepoCat",
+        count: 41,
+        provider: "7tv",
+      },
+      {
+        name: "plink",
+        count: 32,
+        provider: "7tv",
+      },
+      {
+        name: "AngelThump",
+        count: 41,
+        provider: "bttv",
+      },
+      {
+        name: "annytfSad",
+        count: 2,
+        provider: "ttv",
+      },
+    ],
+  },
+  {
+    id: 18,
+    name: "Headdesking1",
+    points: 429,
+    daily_change: 0,
+    daily_change_percent: 0,
+    assets: [
+      {
+        name: "anyaPls",
+        count: 92,
+        provider: "7tv",
+      },
+      {
+        name: "toffeeDinkDonk",
+        count: 6,
+        provider: "7tv",
+      },
+      {
+        name: "SoCute",
+        count: 99,
+        provider: "7tv",
+      },
+      {
+        name: "annyBlankies",
+        count: 42,
+        provider: "bttv",
+      },
+      {
+        name: "annytfHeart",
+        count: 63,
+        provider: "ttv",
+      },
+    ],
+  },
+];
+
+export type { fakeDataEntry };
diff --git a/pages/api/ffz/emotes.ts b/pages/api/ffz/emotes.ts
new file mode 100644
index 0000000..dbc58c9
--- /dev/null
+++ b/pages/api/ffz/emotes.ts
@@ -0,0 +1,30 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { createRedisInstance } from "../../../misc/redis";
+import { getEmoteSet, getGlobalEmotes } from "../../../misc/FFZAPI";
+
+type Data = {
+  [key: string]: any;
+};
+
+export default async function handler(
+  req: NextApiRequest,
+  res: NextApiResponse<Data>
+) {
+  const redis = createRedisInstance();
+
+  try {
+    const channel = req.query.s
+      ? (await getEmoteSet(redis, req.query.s as string)).set.emoticons
+      : undefined;
+    let global = await getGlobalEmotes(redis);
+    // set global emotes to be the three sets within the global object ("3", "4330")
+    global = global.sets["3"].emoticons.concat(global.sets["4330"].emoticons);
+    redis.quit();
+    res.status(200).json({ channel, global });
+  } catch (e) {
+    console.log(e);
+    res
+      .status(500)
+      .json({ error: { message: "FFZ or internal API is down", code: 10300 } });
+  }
+}
diff --git a/pages/api/twitch/emotes.ts b/pages/api/twitch/emotes.ts
new file mode 100644
index 0000000..9452544
--- /dev/null
+++ b/pages/api/twitch/emotes.ts
@@ -0,0 +1,30 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { createRedisInstance } from "../../../misc/redis";
+import { getChannelEmotes, getGlobalEmotes } from "../../../misc/TwitchAPI";
+
+type Data = {
+  [key: string]: any;
+};
+
+export default async function handler(
+  req: NextApiRequest,
+  res: NextApiResponse<Data>
+) {
+  const redis = createRedisInstance();
+
+  try {
+    const channel = req.query.c
+      ? (await getChannelEmotes(redis, req.query.c as string)).data
+      : undefined;
+    const global = (await getGlobalEmotes(redis)).data;
+    redis.quit();
+    res.status(200).json({ channel, global });
+  } catch (e) {
+    console.log(e);
+    res
+      .status(500)
+      .json({
+        error: { message: "Twitch or internal API is down", code: 10100 },
+      });
+  }
+}
diff --git a/pages/index.tsx b/pages/index.tsx
index 197ffd0..25bd22e 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -2,11 +2,10 @@ import { m } from "framer-motion";
 import { ReactElement, useEffect, useState } from "react";
 import HomeLayout from "../layouts/HomeLayout";
 import { homeMain } from "../layouts/NavTemplates";
-import type { NextPageWithLayout } from "./_app";
 import Image from "next/image";
 import Head from "next/head";
 
-const Home: NextPageWithLayout = () => {
+function Home() {
   let api7tvEmotes = `/api/7tv/emotes?c=61ad997effa9aba101bcfddf`;
   const [emotesUrls, setEmotes] = useState([]);
   const [currentEmote, setCurrentEmote] = useState(0);
@@ -15,6 +14,10 @@ const Home: NextPageWithLayout = () => {
     fetch(api7tvEmotes)
       .then((res) => res.json())
       .then((data) => {
+        // if error, return
+        if (data.error) {
+          return;
+        }
         // get all emote URLs
         let emoteUrls = data.channel.user.emote_sets[0].emotes.map(
           (emote: any) => {
@@ -79,49 +82,25 @@ const Home: NextPageWithLayout = () => {
         <title>Home - toffee</title>
       </Head>
       <div className="flex h-full w-full flex-col items-center justify-center">
-        <div className="inline-grid grid-cols-1 gap-10 text-white md:grid-cols-3">
+        <div className="inline-grid grid-cols-1 gap-20 text-white md:grid-cols-3">
           <m.div
-            className="flex flex-col from-purple-400 to-pink-600 font-plusJakarta text-7xl font-semibold sm:text-8xl md:col-span-2"
+            className="flex flex-col from-purple-400 to-pink-600 font-plusJakarta md:col-span-2"
             variants={sloganContainerVariants}
             initial="initial"
             animate="animate"
           >
-            <m.div
-              className="flex flex-row items-center"
-              variants={sloganHeaderVariants}
-            >
-              <h1 className="bg-gradient-to-b bg-clip-text text-transparent">
-                t
-              </h1>
-              <h1>ax-free</h1>
-            </m.div>
-            <m.div
-              className="flex flex-row items-center"
-              variants={sloganHeaderVariants}
-            >
-              <h1 className="bg-gradient-to-tl bg-clip-text text-transparent">
+            <div className="flex flex-row text-8xl font-bold italic">
+              <m.h1 variants={sloganHeaderVariants}>t</m.h1>
+              <m.h1 className="text-orange-400" variants={sloganHeaderVariants}>
                 off
-              </h1>
-              <h1>line</h1>
-            </m.div>
-            <m.div
-              className="flex flex-row items-center"
-              variants={sloganHeaderVariants}
-            >
-              <h1 className="bg-gradient-to-l bg-clip-text text-transparent">
-                e
-              </h1>
-              <h1>mote</h1>
-            </m.div>
-            <m.div
-              className="flex flex-row items-center"
-              variants={sloganHeaderVariants}
-            >
-              <h1 className="bg-gradient-to-bl bg-clip-text text-transparent">
-                e
-              </h1>
-              <h1>xchange</h1>
-            </m.div>
+              </m.h1>
+              <m.h1 variants={sloganHeaderVariants}>ee</m.h1>
+            </div>
+            <div className="text-xl italic">
+              <m.h2 variants={sloganHeaderVariants}>
+                a tax-free offline emote exchange utility
+              </m.h2>
+            </div>
           </m.div>
           <m.div
             className="flex items-center justify-center"
@@ -146,7 +125,7 @@ const Home: NextPageWithLayout = () => {
       </div>
     </>
   );
-};
+}
 
 const sloganContainerVariants = {
   initial: {
@@ -163,7 +142,7 @@ const sloganContainerVariants = {
       bounce: 0.5,
       stiffness: 150,
       delayChildren: 1.0,
-      staggerChildren: 0.45,
+      staggerChildren: 0.1,
     },
   },
 };
@@ -171,9 +150,11 @@ const sloganContainerVariants = {
 const sloganHeaderVariants = {
   initial: {
     opacity: 0,
+    y: -15,
   },
   animate: {
     opacity: 1,
+    y: 0,
   },
 };
 
diff --git a/pages/dashboard/ranking.tsx b/pages/ranking/index.tsx
similarity index 74%
rename from pages/dashboard/ranking.tsx
rename to pages/ranking/index.tsx
index 832718d..5c6bb14 100644
--- a/pages/dashboard/ranking.tsx
+++ b/pages/ranking/index.tsx
@@ -1,9 +1,10 @@
 import { m, Variants } from "framer-motion";
 import Head from "next/head";
+import Link from "next/link";
 import { ReactElement, useEffect, useState } from "react";
 import Loading from "../../components/common/Loading";
 import DashLayout from "../../layouts/DashLayout";
-import { fakeDataEntry } from "../api/fakeRanking";
+import { fakeDataEntry } from "../api/fakeUsers";
 
 function Ranking() {
   const [sortBy, setSortBy] = useState("netWorth");
@@ -14,7 +15,7 @@ function Ranking() {
   useEffect(() => {
     setDataLoaded(false);
     // fetch data from api on change to sort method
-    fetch(`/api/fakeRanking?s=${sortBy}&a=${sortAsc}`)
+    fetch(`/api/fakeUsers?s=${sortBy}&a=${sortAsc}`)
       .then((res) => res.json())
       .then((data) => {
         setFakeData(data.data);
@@ -78,13 +79,13 @@ function Ranking() {
         <title>Ranking - toffee</title>
       </Head>
       <div className="flex w-full justify-center">
-        <div className="ml-3 flex w-full flex-col items-center justify-start font-robotoMono font-semibold lg:ml-0">
+        <div className="ml-3 flex w-full max-w-7xl flex-col items-center justify-start font-plusJakarta font-semibold lg:ml-0">
           {/* hidden if smaller than lg */}
           <m.h1
-            className="hidden bg-gradient-to-tr from-purple-500 to-purple-100 bg-clip-text py-10 text-center font-plusJakarta text-5xl font-bold text-white text-transparent lg:block lg:text-6xl"
+            className="hidden py-10 text-center text-5xl font-normal text-pink-300 lg:block lg:text-6xl"
             variants={headerVariants}
           >
-            Top Investors
+            top investors
           </m.h1>
           {/* TODO: responsive for extremely skinny displays (i.e. galaxy fold), or really for mobile entirely so info is not lost */}
           <m.div
@@ -162,39 +163,47 @@ function Ranking() {
                 >
                   {
                     // generate table rows
-                    fakeData.map((entry: fakeDataEntry, index) => {
-                      // if daily change is negative, make it red
-                      let changeClass = " text-lime-500";
-                      if (entry.dailyChangePercent < 0) {
-                        changeClass = " text-red-500";
+                    fakeData.map(
+                      (entry: { [key: string]: any }, index: number) => {
+                        // if daily change is negative, make it red
+                        let changeClass = " text-lime-500";
+                        if (entry.daily_change_percent < 0) {
+                          changeClass = " text-red-500";
+                        }
+                        return (
+                          <m.div
+                            className="inline-grid w-full grid-flow-col grid-cols-[1fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-700 px-5 py-2 text-right md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"
+                            key={entry.id}
+                            variants={rankingDataLineVariants}
+                          >
+                            <h1 className="text-left md:text-center">
+                              {index + 1}
+                            </h1>
+                            <Link
+                              href={`/user/${entry.name}`}
+                              className="overflow-hidden"
+                            >
+                              <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left">
+                                {entry.name}
+                              </h1>
+                            </Link>
+                            <h1>{entry.net_worth.toLocaleString("en-US")}</h1>
+                            <h1 className="hidden md:block">
+                              {entry.points.toLocaleString("en-US")}
+                            </h1>
+                            <h1 className="hidden md:block">
+                              {entry.shares.toLocaleString("en-US")}
+                            </h1>
+                            <h1 className={changeClass}>
+                              {(
+                                Math.round(entry.daily_change_percent * 1000) /
+                                10
+                              ).toFixed(1) + "%"}
+                            </h1>
+                          </m.div>
+                        );
                       }
-                      return (
-                        <m.div
-                          className="inline-grid w-full grid-flow-col grid-cols-[1fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-700 px-5 py-2 text-right md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"
-                          key={entry.id}
-                          variants={rankingDataLineVariants}
-                        >
-                          <h1 className="text-left md:text-center">
-                            {index + 1}
-                          </h1>
-                          <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left">
-                            {entry.name}
-                          </h1>
-                          <h1>{entry.netWorth.toLocaleString("en-US")}</h1>
-                          <h1 className="hidden md:block">
-                            {entry.points.toLocaleString("en-US")}
-                          </h1>
-                          <h1 className="hidden md:block">
-                            {entry.shares.toLocaleString("en-US")}
-                          </h1>
-                          <h1 className={changeClass}>
-                            {(
-                              Math.round(entry.dailyChangePercent * 1000) / 10
-                            ).toFixed(1) + "%"}
-                          </h1>
-                        </m.div>
-                      );
-                    })
+                    )
                   }
                 </m.div>
               )
diff --git a/pages/user/[username]/index.tsx b/pages/user/[username]/index.tsx
new file mode 100644
index 0000000..c0057c8
--- /dev/null
+++ b/pages/user/[username]/index.tsx
@@ -0,0 +1,486 @@
+import { m } from "framer-motion";
+import Head from "next/head";
+import { useRouter } from "next/router";
+import { ReactElement, useEffect, useState } from "react";
+import DashLayout from "../../../layouts/DashLayout";
+import Image from "next/image";
+import Loading from "../../../components/common/Loading";
+
+// TODO: Animations
+
+function UserPage() {
+  const [channelEmotes, setChannelEmotes] = useState<{
+    [key: string]: { [key: string]: string };
+  }>({});
+  const [userData, setUserData] = useState<{ [key: string]: any }>({});
+  const [errorCode, setErrorCode] = useState<number | null>(null);
+  const router = useRouter();
+  const { username } = router.query;
+  const title = username ? `${username} - toffee` : "toffee";
+
+  useEffect(() => {
+    if (!router.isReady) return;
+    fetch("/api/7tv/emotes?c=61ad997effa9aba101bcfddf")
+      .then((res) => res.json())
+      .then((data) => {
+        // if error, return
+        if (data.error) {
+          setErrorCode(data.error.code);
+          return;
+        }
+        // construct js object with emote names as keys and emote urls as values
+        let emotes: { [key: string]: string } = {};
+        data.channel.user.emote_sets[0].emotes.forEach((emote: any) => {
+          let base_url = emote.data.host.url;
+          // get the largest emote size, append it to the base url
+          let largest = emote.data.host.files[emote.data.host.files.length - 1];
+          emotes[emote.data.name] = `https:${base_url}/${largest.name}`;
+        });
+        // same for global emotes
+        data.global.namedEmoteSet.emotes.forEach((emote: any) => {
+          let base_url = emote.data.host.url;
+          let largest = emote.data.host.files[emote.data.host.files.length - 1];
+          emotes[emote.data.name] = `https:${base_url}/${largest.name}`;
+        });
+        // set 7tv key to channelEmotes
+        setChannelEmotes((prev) => ({ ...prev, "7tv": emotes }));
+      });
+    fetch("/api/bttv/emotes?c=56418014")
+      .then((res) => res.json())
+      .then((data) => {
+        if (data.error) {
+          setErrorCode(data.error.code);
+          return;
+        }
+        let emotes: { [key: string]: string } = {};
+        data.channel.forEach((emote: any) => {
+          emotes[emote.code] = `https://cdn.betterttv.net/emote/${emote.id}/3x`;
+        });
+        data.global.forEach((emote: any) => {
+          emotes[emote.code] = `https://cdn.betterttv.net/emote/${emote.id}/3x`;
+        });
+        // add as bttv key to channelEmotes
+        setChannelEmotes((prev) => ({ ...prev, bttv: emotes }));
+      });
+    fetch("/api/ffz/emotes?s=341402")
+      .then((res) => res.json())
+      .then((data) => {
+        if (data.error) {
+          setErrorCode(data.error.code);
+          return;
+        }
+        let emotes: { [key: string]: string } = {};
+        data.channel.forEach((emote: any) => {
+          // ffz emotes don't have all sizes available, so we need to get the largest one by taking the largest key in the urls object
+          emotes[emote.name] = `https:${
+            emote.urls[
+              Math.max(...Object.keys(emote.urls).map((k) => parseInt(k)))
+            ]
+          }`;
+        });
+        data.global.forEach((emote: any) => {
+          emotes[emote.name] = `https:${
+            emote.urls[
+              Math.max(...Object.keys(emote.urls).map((k) => parseInt(k)))
+            ]
+          }`;
+        });
+        // add as ffz key to channelEmotes
+        setChannelEmotes((prev) => ({ ...prev, ffz: emotes }));
+      });
+    fetch("/api/twitch/emotes?c=56418014")
+      .then((res) => res.json())
+      .then((data) => {
+        if (data.error) {
+          setErrorCode(data.error.code);
+          return;
+        }
+        let emotes: { [key: string]: string } = {};
+        data.channel.forEach((emote: any) => {
+          emotes[emote.name] = emote.images["url_4x"];
+        });
+        data.global.forEach((emote: any) => {
+          emotes[emote.name] = emote.images["url_4x"];
+        });
+        // add as twitch key to channelEmotes
+        setChannelEmotes((prev) => ({ ...prev, ttv: emotes }));
+      });
+    fetch(`/api/fakeUsers?u=${username}`)
+      .then((res) => res.json())
+      .then((data) => {
+        if (data.error) {
+          setErrorCode(data.error.code);
+        }
+        setUserData(data.data);
+      });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [router.isReady]);
+
+  if (errorCode !== null) {
+    // 20000 = user not found
+    // 10000 = 7tv api error
+    // 10100 = Twitch api error
+    // 10200 = BTTV api error
+    // 10300 = FFZ api error
+    const errorMsg = errorCode === 20000 ? "User not found" : "API error";
+    return (
+      <m.div
+        className="flex h-screen w-full items-center justify-center text-3xl"
+        initial={{ opacity: 0, y: -50 }}
+        animate={{ opacity: 1, y: 0, transition: { duration: 1.0 } }}
+        exit={{ opacity: 0, y: -25 }}
+      >
+        <p>{errorMsg}</p>
+      </m.div>
+    );
+  }
+
+  // if json is empty, and if channelEmotes is incomplete, show loading screen
+  if (
+    Object.keys(channelEmotes).length < 4 ||
+    !userData ||
+    Object.keys(userData).length === 0
+  ) {
+    return (
+      <div className="flex h-screen w-full items-center justify-center text-3xl">
+        <Loading />
+      </div>
+    );
+  }
+  console.log(channelEmotes);
+  return (
+    <>
+      <Head>
+        <title>{title}</title>
+        <meta
+          name="description"
+          content={`${username}'s portfolio on toffee`}
+        />
+      </Head>
+      <div className="flex justify-center">
+        <div className="mt-7 inline-grid w-[calc(100%-40px)] max-w-5xl grid-cols-10 gap-3 pl-2 font-plusJakarta lg:mt-12 lg:pl-0 lg:pr-2">
+          {/*  User "banner"  */}
+          <div className="col-span-10 mb-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3">
+            <div className="flex items-center justify-between p-4">
+              <div className="flex flex-row items-center">
+                <div className="relative bottom-[70px] w-[169px]">
+                  <Image
+                    src={userData.avatar_url}
+                    alt="User avatar"
+                    width={140}
+                    height={140}
+                    priority
+                    className="absolute rounded-lg border-4"
+                    style={{
+                      borderColor: userData.badges[0]
+                        ? userData.badges[0].color
+                        : "grey",
+                      // "glow" effect
+                      boxShadow: `0px 0px 20px 1px ${
+                        userData.badges[0]
+                          ? userData.badges[0].color
+                          : "transparent"
+                      }`,
+                    }}
+                  />
+                </div>
+                <div className="flex-col">
+                  <h1 className="text-4xl font-semibold text-white">
+                    {userData.name}
+                  </h1>
+                  {/*  User's badges  */}
+                  <div className="mt-1 flex flex-row text-sm">
+                    {userData.badges ? (
+                      userData.badges.map(
+                        (badge: {
+                          name: string;
+                          color: string;
+                          priority: number;
+                        }) => {
+                          return (
+                            <div
+                              style={{ backgroundColor: badge.color }}
+                              className="mr-1 rounded-md bg-purple-500 px-2"
+                              key={badge.name}
+                            >
+                              <span className="text-white">{badge.name}</span>
+                            </div>
+                          );
+                        }
+                      )
+                    ) : (
+                      <></>
+                    )}
+                  </div>
+                </div>
+              </div>
+              <div className="hidden md:block">
+                <h1>
+                  <span className="text-4xl font-semibold text-zinc-400">
+                    $
+                  </span>
+                  <span className="text-4xl text-white">
+                    {userData.net_worth.toLocaleString("en-US")}
+                  </span>
+                </h1>
+              </div>
+            </div>
+          </div>
+          {/*  Main Container  */}
+          <div className="col-span-10 inline-grid grid-cols-7 gap-3 rounded-2xl lg:col-span-7">
+            {/*  User's Rank/Graph  */}
+            <div className="col-span-7 rounded-2xl bg-zinc-800 bg-opacity-70">
+              <div className="flex flex-row items-center justify-between p-5">
+                <div className="flex-col px-2">
+                  <h1 className="mb-1 whitespace-nowrap text-center text-xl font-medium text-white underline">
+                    Global Rank
+                  </h1>
+                  <div className="flex items-center text-3xl font-bold">
+                    <span className="text-zinc-400">#</span>
+                    <span className="text-white">
+                      {userData.rank.toLocaleString("en-US")}
+                    </span>
+                  </div>
+                </div>
+                <div className="hidden md:block">
+                  <Image
+                    src="/img/well_drawn_rank_chart.webp"
+                    alt="Rank chart"
+                    width={497}
+                    height={100}
+                  />
+                </div>
+                <div className="md:hidden">
+                  <h1>
+                    <span className="text-3xl font-semibold text-zinc-400 sm:text-4xl">
+                      $
+                    </span>
+                    <span className="text-3xl text-white sm:text-4xl">
+                      {userData.net_worth.toLocaleString("en-US")}
+                    </span>
+                  </h1>
+                </div>
+              </div>
+            </div>
+            {/*  User's Assets  */}
+            <div className="col-span-7 flex flex-col rounded-2xl bg-zinc-800 bg-opacity-70">
+              {/*  User's Assets Header  */}
+              <div className="h-11 w-full rounded-t-2xl bg-pink-400">
+                <h1 className="m-1 text-center text-2xl font-bold">
+                  Top Assets
+                </h1>
+              </div>
+              {/*  User's Assets Body  */}
+              <div className="inline-grid grid-cols-2 items-center justify-start gap-2 p-5 sm:grid-cols-3 xl:grid-cols-4">
+                {errorCode === 20000 ? (
+                  <h1 className=" text-zinc-400">{`Could not load assets`}</h1>
+                ) : (
+                  userData.assets.map(
+                    (asset: {
+                      name: string;
+                      count: number;
+                      provider: string;
+                    }) => (
+                      <div
+                        className="flex items-center justify-center"
+                        key={asset.name}
+                      >
+                        <div className="flex h-44 w-full max-w-[256px] flex-col items-center rounded-xl bg-zinc-900 bg-opacity-80 p-2">
+                          <div className="mt-2 mb-2 h-24 w-24">
+                            <div className="flex h-full w-full items-center justify-start p-2">
+                              {
+                                // if error code is 10000 or emote does not exist, show placeholder image
+                                errorCode === 10000 ||
+                                channelEmotes[asset.provider] === undefined ||
+                                channelEmotes[asset.provider][asset.name] ===
+                                  undefined ? (
+                                  <h1 className="text-center text-zinc-400">{`404 :(`}</h1>
+                                ) : (
+                                  <Image
+                                    src={
+                                      channelEmotes[asset.provider][
+                                        asset.name
+                                      ] ?? ""
+                                    }
+                                    alt={asset.name}
+                                    width={100}
+                                    height={100}
+                                    className="max-h-[100px]"
+                                  />
+                                )
+                              }
+                              {/*  Fix asset count to bottom right of image  */}
+                              <div className="relative rounded-full bg-zinc-900 bg-opacity-80 p-1">
+                                <p
+                                  className="absolute -bottom-10 -right-2 -rotate-12 text-lg font-bold text-white"
+                                  style={{ textShadow: "0px 0px 4px black" }}
+                                >
+                                  x{asset.count}
+                                </p>
+                              </div>
+                            </div>
+                          </div>
+                          <div className="flex w-full flex-row items-center justify-center">
+                            {
+                              // show provider logo (7tv, bttv, ffz, ttv)
+                              asset.provider === "7tv" ? (
+                                <div className="mr-1 pt-[1px] text-7tv ">
+                                  <SevenTVLogo />
+                                </div>
+                              ) : asset.provider === "bttv" ? (
+                                <div className="mr-1 pt-[1px] text-bttv">
+                                  <BTTVLogo />
+                                </div>
+                              ) : asset.provider === "ffz" ? (
+                                <div className="h-5 w-6 text-white">
+                                  <FFZLogo />
+                                </div>
+                              ) : (
+                                <div className="mr-1 w-4 pt-[1px] text-ttv">
+                                  <TwitchLogo />
+                                </div>
+                              )
+                            }
+                            <p className="text-md max-w-[80%] overflow-hidden overflow-ellipsis whitespace-nowrap font-bold text-white">
+                              {asset.name}
+                            </p>
+                          </div>
+                        </div>
+                      </div>
+                    )
+                  )
+                )}
+              </div>
+            </div>
+          </div>
+          {/*  Sidebar  */}
+          <div className="col-span-10 flex flex-col justify-start md:flex-row lg:col-span-3 lg:flex-col">
+            <div className="center mb-3 mr-3 inline-grid grid-cols-2 gap-3 rounded-2xl bg-zinc-800 bg-opacity-70 p-5 text-xl font-medium lg:mr-0">
+              {/*  User's Stats, left side is label, right side is value  */}
+              <h1>Points</h1>
+              <h1>{userData.points.toLocaleString("en-US")}</h1>
+              <h1>Shares</h1>
+              <h1>{userData.shares.toLocaleString("en-US")}</h1>
+              <h1>Trades</h1>
+              <h1>{(userData.trades ?? 0).toLocaleString("en-US")}</h1>
+              <h1>Peak rank</h1>
+              <h1>{(userData.peak_rank ?? 0).toLocaleString("en-US")}</h1>
+              <h1>Joined</h1>
+              <h1>
+                {new Date(userData.joined ?? 0).toLocaleDateString("en-US", {
+                  year: "numeric",
+                  month: "short",
+                })}
+              </h1>
+            </div>
+            {/*  User's Favorite Emote  */}
+            <div className="flex flex-col rounded-2xl bg-zinc-800 bg-opacity-70">
+              <div className="h-11 w-full rounded-t-2xl bg-pink-400">
+                <h1 className="m-1 text-center text-2xl font-bold">
+                  Favorite Emote
+                </h1>
+              </div>
+              <div>
+                <p className="m-5 text-lg text-zinc-400">
+                  This user has not yet set a favorite emote.
+                </p>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </>
+  );
+}
+
+const SevenTVLogo = () => {
+  return (
+    <svg viewBox="0 0 109.6 80.9" width="1em">
+      <g>
+        <path
+          d="M84.1,22.2l5-8.7,2.7-4.6L86.8.2V0H60.1l5,8.7,5,8.7,2.8,4.8H84.1"
+          fill="currentColor"
+        ></path>
+        <path
+          d="M29,80.6l5-8.7,5-8.7,5-8.7,5-8.7,5-8.7,5-8.7L62.7,22l-5-8.7-5-8.7L49.9.1H7.7l-5,8.7L0,13.4l5,8.7v.2h32l-5,8.7-5,8.7-5,8.7-5,8.7-5,8.7L8.5,72l5,8.7v.2H29"
+          fill="currentColor"
+        ></path>
+        <path
+          d="M70.8,80.6H86.1l5-8.7,5-8.7,5-8.7,5-8.7,3.5-6-5-8.7v-.2H89.2l-5,8.7-5,8.7-.7,1.3-5-8.7-5-8.7-.7-1.3-5,8.7-5,8.7L55,53.1l5,8.7,5,8.7,5,8.7.8,1.4"
+          fill="currentColor"
+        ></path>
+      </g>
+    </svg>
+  );
+};
+
+const FFZLogo = () => {
+  return (
+    <svg viewBox="0 0 396 396">
+      <path
+        d="m150.06,151.58c-.77-6.33.31-12.45,1.99-18.5,2.37-8.51,9.11-14.22,15.62-18.44,8.71-5.65,18.98-8.6,30.02-8.89,7.26-.19,12.89,3.13,18.17,6.2,3.98,2.32,7.8,6.66,10.25,11.43,2.99,5.83,5.92,11.51,7.05,18.18,1.17,6.9,4.69,13.3,9.05,18.55,4.61,5.55,5.63,11.86,5.53,18.1-.15,8.77,3.32,16.07,6.61,23.64.93,2.14,2.15,3.87,4.76,3.08,2.58-.77,4.04-2.69,3.78-5.44-.32-3.41-.64-6.86-1.43-10.18-1.01-4.18-1.54-8.31-1.45-12.65.19-9.37,6.06-15.83,12.06-21.43,5.81-5.44,9.29-4.23,15.15,1.48,7.52,7.32,14.31,15.22,18.49,24.91,3.86,8.94,7.62,17.97,10.54,27.24,1.79,5.7,2.26,11.9,2.71,17.92.35,4.7-2.69,8.43-6.01,11.17-12.05,9.97-24.04,20.06-38.64,26.39-3.06,1.32-5.93,3.65-8.27,5.9-4.3,4.13-8.7,4.8-14.22,3.81-6.67-1.2-12.21,2.2-17.76,5.37-10.83,6.19-21.98,11.44-34.99,10.56-2.65-.18-5.35-.19-7.99.05-9.67.89-18.35-1.86-26.44-6.91-5.06-3.16-10.74-5.27-15.39-9.12-.62-.51-1.58-.75-1.96-1.37-3.61-5.84-8.03-5.41-13.72-2.82-7.66,3.48-8.07,3.02-13.2-3.77-3.48-4.6-8.91-6.59-13.27-10.01-11.43-8.97-22.52-18.11-29.39-31.42-2.17-4.21-2.9-8.38-2.59-12.82.76-10.87,1-21.85,7.82-31.3,6.01-8.32,10.79-17.68,19.29-23.9,5.9-4.32,10.15-2.9,14.27,3.26,6.93,10.36,7.99,21.4,4.42,33.03-1.46,4.76-.62,9.52-.8,14.28-.09,2.23,2.26,4.61,3.36,4.24,2.47-.83,5.83.99,7.52-2.37,5.96-11.87,14.26-22.67,17.36-35.82,1.65-7.02,2.21-14.34,1.72-21.63Z"
+        fill="currentColor"
+      />
+    </svg>
+  );
+};
+
+const BTTVLogo = () => {
+  return (
+    <svg viewBox="0 0 300 300" height="1em">
+      <path
+        fill="transparent"
+        d="M249.771 150A99.771 99.922 0 0 1 150 249.922 99.771 99.922 0 0 1 50.229 150 99.771 99.922 0 0 1 150 50.078 99.771 99.922 0 0 1 249.771 150Z"
+      ></path>
+      <path
+        fill="currentColor"
+        d="M150 1.74C68.409 1.74 1.74 68.41 1.74 150S68.41 298.26 150 298.26h148.26V150.17h-.004c0-.057.004-.113.004-.17C298.26 68.409 231.59 1.74 150 1.74zm0 49c55.11 0 99.26 44.15 99.26 99.26 0 55.11-44.15 99.26-99.26 99.26-55.11 0-99.26-44.15-99.26-99.26 0-55.11 44.15-99.26 99.26-99.26z"
+      ></path>
+      <path
+        fill="currentColor"
+        d="M161.388 70.076c-10.662 0-19.42 7.866-19.42 17.67 0 9.803 8.758 17.67 19.42 17.67 10.662 0 19.42-7.867 19.42-17.67 0-9.804-8.758-17.67-19.42-17.67zm45.346 24.554-.02.022-.004.002c-5.402 2.771-11.53 6.895-18.224 11.978l-.002.002-.004.002c-25.943 19.766-60.027 54.218-80.344 80.33h-.072l-1.352 1.768c-5.114 6.69-9.267 12.762-12.098 18.006l-.082.082.022.021v.002l.004.002.174.176.052-.053.102.053-.07.072c30.826 30.537 81.213 30.431 111.918-.273 30.783-30.784 30.8-81.352.04-112.152l-.005-.004zM87.837 142.216c-9.803 0-17.67 8.758-17.67 19.42 0 10.662 7.867 19.42 17.67 19.42 9.804 0 17.67-8.758 17.67-19.42 0-10.662-7.866-19.42-17.67-19.42z"
+      ></path>
+    </svg>
+  );
+};
+
+const TwitchLogo = () => {
+  return (
+    <svg x="0px" y="0px" viewBox="0 0 2400 2800">
+      <g>
+        <polygon
+          className="fill-white"
+          points="2200,1300 1800,1700 1400,1700 1050,2050 1050,1700 600,1700 600,200 2200,200"
+        />
+        <g>
+          <g id="Layer_1-2">
+            <path
+              fill="currentColor"
+              d="M500,0L0,500v1800h600v500l500-500h400l900-900V0H500z M2200,1300l-400,400h-400l-350,350v-350H600V200h1600     V1300z"
+            />
+            <rect
+              x="1700"
+              y="550"
+              fill="currentColor"
+              width="200"
+              height="600"
+            />
+            <rect
+              x="1150"
+              y="550"
+              fill="currentColor"
+              width="200"
+              height="600"
+            />
+          </g>
+        </g>
+      </g>
+    </svg>
+  );
+};
+
+UserPage.getLayout = function getLayout(page: ReactElement) {
+  return <DashLayout>{page}</DashLayout>;
+};
+
+export default UserPage;
diff --git a/public/img/well_drawn_rank_chart.webp b/public/img/well_drawn_rank_chart.webp
new file mode 100644
index 0000000..39d254f
Binary files /dev/null and b/public/img/well_drawn_rank_chart.webp differ
diff --git a/tailwind.config.js b/tailwind.config.js
index 325ade7..0685455 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -13,6 +13,11 @@ module.exports = {
         robotoMono: ["Roboto Mono", "monospace"],
         minecraft: ["Minecraft", "Roboto", "sans-serif"],
       },
+      colors: {
+        "7tv": "#4fc2bc",
+        bttv: "#d50014",
+        ttv: "#9146FF",
+      },
     },
   },
   plugins: [],