diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts
index 41ad8141..4a1e04f8 100644
--- a/functions/src/place-bet.ts
+++ b/functions/src/place-bet.ts
@@ -40,9 +40,6 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
         return { status: 'error', message: 'User not found' }
       const user = userSnap.data() as User
 
-      if (user.balance < amount)
-        return { status: 'error', message: 'Insufficient balance' }
-
       const contractDoc = firestore.doc(`contracts/${contractId}`)
       const contractSnap = await transaction.get(contractDoc)
       if (!contractSnap.exists)
@@ -58,6 +55,10 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
       )
       const yourBets = yourBetsSnap.docs.map((doc) => doc.data() as Bet)
 
+      const loanAmount = getLoanAmount(yourBets, amount)
+      if (user.balance < amount - loanAmount)
+        return { status: 'error', message: 'Insufficient balance' }
+
       if (outcomeType === 'FREE_RESPONSE') {
         const answerSnap = await transaction.get(
           contractDoc.collection('answers').doc(outcome)
@@ -70,8 +71,6 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
         .collection(`contracts/${contractId}/bets`)
         .doc()
 
-      const loanAmount = getLoanAmount(yourBets, amount)
-
       const { newBet, newPool, newTotalShares, newTotalBets, newBalance } =
         outcomeType === 'BINARY'
           ? mechanism === 'dpm-2'
diff --git a/functions/src/utils.ts b/functions/src/utils.ts
index 0e87538a..f34db1c8 100644
--- a/functions/src/utils.ts
+++ b/functions/src/utils.ts
@@ -68,8 +68,7 @@ const updateUserBalance = (
 }
 
 export const payUser = (userId: string, payout: number, isDeposit = false) => {
-  if (!isFinite(payout) || payout <= 0)
-    throw new Error('Payout is not positive: ' + payout)
+  if (!isFinite(payout)) throw new Error('Payout is not finite: ' + payout)
 
   return updateUserBalance(userId, payout, isDeposit)
 }
diff --git a/web/pages/activity.tsx b/web/components/activity-feed.tsx
similarity index 74%
rename from web/pages/activity.tsx
rename to web/components/activity-feed.tsx
index bab58328..bfd4cc1c 100644
--- a/web/pages/activity.tsx
+++ b/web/components/activity-feed.tsx
@@ -1,9 +1,12 @@
 import _ from 'lodash'
-import { ContractFeed, ContractSummaryFeed } from '../components/contract-feed'
-import { Page } from '../components/page'
+import {
+  ContractActivityFeed,
+  ContractFeed,
+  ContractSummaryFeed,
+} from './contract-feed'
 import { Contract } from '../lib/firebase/contracts'
 import { Comment } from '../lib/firebase/comments'
-import { Col } from '../components/layout/col'
+import { Col } from './layout/col'
 import { Bet } from '../../common/bet'
 
 const MAX_ACTIVE_CONTRACTS = 75
@@ -72,30 +75,44 @@ export function findActiveContracts(
 
 export function ActivityFeed(props: {
   contracts: Contract[]
-  contractBets: Bet[][]
-  contractComments: Comment[][]
+  recentBets: Bet[]
+  recentComments: Comment[]
+  loadBetAndCommentHistory?: boolean
 }) {
-  const { contracts, contractBets, contractComments } = props
+  const { contracts, recentBets, recentComments, loadBetAndCommentHistory } =
+    props
 
-  return contracts.length > 0 ? (
+  const groupedBets = _.groupBy(recentBets, (bet) => bet.contractId)
+  const groupedComments = _.groupBy(
+    recentComments,
+    (comment) => comment.contractId
+  )
+
+  return (
     <Col className="items-center">
-      <Col className="w-full max-w-3xl">
+      <Col className="w-full">
         <Col className="w-full divide-y divide-gray-300 self-center bg-white">
-          {contracts.map((contract, i) => (
+          {contracts.map((contract) => (
             <div key={contract.id} className="py-6 px-2 sm:px-4">
-              <ContractFeed
-                contract={contract}
-                bets={contractBets[i]}
-                comments={contractComments[i]}
-                feedType="activity"
-              />
+              {loadBetAndCommentHistory ? (
+                <ContractFeed
+                  contract={contract}
+                  bets={groupedBets[contract.id] ?? []}
+                  comments={groupedComments[contract.id] ?? []}
+                  feedType="activity"
+                />
+              ) : (
+                <ContractActivityFeed
+                  contract={contract}
+                  bets={groupedBets[contract.id] ?? []}
+                  comments={groupedComments[contract.id] ?? []}
+                />
+              )}
             </div>
           ))}
         </Col>
       </Col>
     </Col>
-  ) : (
-    <></>
   )
 }
 
@@ -116,11 +133,3 @@ export function SummaryActivityFeed(props: { contracts: Contract[] }) {
     </Col>
   )
 }
-
-export default function ActivityPage() {
-  return (
-    <Page>
-      <ActivityFeed contracts={[]} contractBets={[]} contractComments={[]} />
-    </Page>
-  )
-}
diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx
index e36962a2..8f20c0ab 100644
--- a/web/components/amount-input.tsx
+++ b/web/components/amount-input.tsx
@@ -14,7 +14,7 @@ export function AmountInput(props: {
   onChange: (newAmount: number | undefined) => void
   error: string | undefined
   setError: (error: string | undefined) => void
-  contractId: string | undefined
+  contractIdForLoan: string | undefined
   minimumAmount?: number
   disabled?: boolean
   className?: string
@@ -27,7 +27,7 @@ export function AmountInput(props: {
     onChange,
     error,
     setError,
-    contractId,
+    contractIdForLoan,
     disabled,
     className,
     inputClassName,
@@ -37,14 +37,13 @@ export function AmountInput(props: {
 
   const user = useUser()
 
-  const userBets = useUserContractBets(user?.id, contractId) ?? []
+  const userBets = useUserContractBets(user?.id, contractIdForLoan) ?? []
   const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
   const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
 
-  const loanAmount = Math.min(
-    amount ?? 0,
-    MAX_LOAN_PER_CONTRACT - prevLoanAmount
-  )
+  const loanAmount = contractIdForLoan
+    ? Math.min(amount ?? 0, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
+    : 0
 
   const onAmountChange = (str: string) => {
     if (str.includes('-')) {
@@ -58,7 +57,12 @@ export function AmountInput(props: {
 
     onChange(str ? amount : undefined)
 
-    if (user && user.balance < amount) {
+    const loanAmount = contractIdForLoan
+      ? Math.min(amount, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
+      : 0
+    const amountNetLoan = amount - loanAmount
+
+    if (user && user.balance < amountNetLoan) {
       setError('Insufficient balance')
     } else if (minimumAmount && amount < minimumAmount) {
       setError('Minimum amount: ' + formatMoney(minimumAmount))
@@ -99,7 +103,7 @@ export function AmountInput(props: {
       )}
       {user && (
         <Col className="gap-3 text-sm">
-          {contractId && (
+          {contractIdForLoan && (
             <Row className="items-center justify-between gap-2 text-gray-500">
               <Row className="items-center gap-2">
                 Amount loaned{' '}
diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx
new file mode 100644
index 00000000..3a315d38
--- /dev/null
+++ b/web/components/analytics/charts.tsx
@@ -0,0 +1,49 @@
+import { ResponsiveLine } from '@nivo/line'
+import dayjs from 'dayjs'
+import _ from 'lodash'
+import { useWindowSize } from '../../hooks/use-window-size'
+
+export function DailyCountChart(props: {
+  startDate: number
+  dailyCounts: number[]
+  small?: boolean
+}) {
+  const { dailyCounts, startDate, small } = props
+  const { width } = useWindowSize()
+
+  const dates = dailyCounts.map((_, i) =>
+    dayjs(startDate).add(i, 'day').toDate()
+  )
+
+  const points = _.zip(dates, dailyCounts).map(([date, betCount]) => ({
+    x: date,
+    y: betCount,
+  }))
+  const data = [{ id: 'Count', data: points, color: '#11b981' }]
+
+  return (
+    <div
+      className="w-full"
+      style={{ height: !small && (!width || width >= 800) ? 400 : 250 }}
+    >
+      <ResponsiveLine
+        data={data}
+        yScale={{ type: 'linear', stacked: false }}
+        xScale={{
+          type: 'time',
+        }}
+        axisBottom={{
+          format: (date) => dayjs(date).format('MMM DD'),
+        }}
+        colors={{ datum: 'color' }}
+        pointSize={width && width >= 800 ? 10 : 0}
+        pointBorderWidth={1}
+        pointBorderColor="#fff"
+        enableSlices="x"
+        enableGridX={!!width && width >= 800}
+        enableArea
+        margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
+      />
+    </div>
+  )
+}
diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx
index bad7efc0..e426c370 100644
--- a/web/components/answers/answer-bet-panel.tsx
+++ b/web/components/answers/answer-bet-panel.tsx
@@ -49,11 +49,6 @@ export function AnswerBetPanel(props: {
   async function submitBet() {
     if (!user || !betAmount) return
 
-    if (user.balance < betAmount) {
-      setError('Insufficient balance')
-      return
-    }
-
     setError(undefined)
     setIsSubmitting(true)
 
@@ -103,11 +98,11 @@ export function AnswerBetPanel(props: {
 
   return (
     <Col className={clsx('px-2 pb-2 pt-4 sm:pt-0', className)}>
-      <Row className="self-stretch items-center justify-between">
+      <Row className="items-center justify-between self-stretch">
         <div className="text-xl">Buy this answer</div>
 
         <button className="btn-ghost btn-circle" onClick={closePanel}>
-          <XIcon className="w-8 h-8 text-gray-500 mx-auto" aria-hidden="true" />
+          <XIcon className="mx-auto h-8 w-8 text-gray-500" aria-hidden="true" />
         </button>
       </Row>
       <div className="my-3 text-left text-sm text-gray-500">Amount </div>
@@ -119,10 +114,10 @@ export function AnswerBetPanel(props: {
         setError={setError}
         disabled={isSubmitting}
         inputRef={inputRef}
-        contractId={contract.id}
+        contractIdForLoan={contract.id}
       />
-      <Col className="gap-3 mt-3 w-full">
-        <Row className="justify-between items-center text-sm">
+      <Col className="mt-3 w-full gap-3">
+        <Row className="items-center justify-between text-sm">
           <div className="text-gray-500">Probability</div>
           <Row>
             <div>{formatPercent(initialProb)}</div>
@@ -131,8 +126,8 @@ export function AnswerBetPanel(props: {
           </Row>
         </Row>
 
-        <Row className="justify-between items-start text-sm gap-2">
-          <Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
+        <Row className="items-start justify-between gap-2 text-sm">
+          <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
             <div>Payout if chosen</div>
             <InfoTooltip
               text={`Current payout for ${formatWithCommas(
@@ -142,7 +137,7 @@ export function AnswerBetPanel(props: {
               )} shares`}
             />
           </Row>
-          <Row className="flex-wrap justify-end items-end gap-2">
+          <Row className="flex-wrap items-end justify-end gap-2">
             <span className="whitespace-nowrap">
               {formatMoney(currentPayout)}
             </span>
diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx
index a1f8b1d1..ae79cdf3 100644
--- a/web/components/answers/answer-resolve-panel.tsx
+++ b/web/components/answers/answer-resolve-panel.tsx
@@ -71,9 +71,9 @@ export function AnswerResolvePanel(props: {
       : 'btn-disabled'
 
   return (
-    <Col className="gap-4 p-4 bg-gray-50 rounded">
+    <Col className="gap-4 rounded bg-gray-50 p-4">
       <div>Resolve your market</div>
-      <Col className="sm:flex-row sm:items-center gap-4">
+      <Col className="gap-4 sm:flex-row sm:items-center">
         <ChooseCancelSelector
           className="sm:!flex-row sm:items-center"
           selected={resolveOption}
diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx
index d30499f6..f1af8692 100644
--- a/web/components/answers/answers-panel.tsx
+++ b/web/components/answers/answers-panel.tsx
@@ -98,9 +98,9 @@ export function AnswersPanel(props: {
       ))}
 
       {sortedAnswers.length === 0 ? (
-        <div className="text-gray-500 p-4">No answers yet...</div>
+        <div className="p-4 text-gray-500">No answers yet...</div>
       ) : (
-        <div className="text-gray-500 self-end p-4">
+        <div className="self-end p-4 text-gray-500">
           None of the above:{' '}
           {formatPercent(getDpmOutcomeProbability(contract.totalShares, '0'))}
         </div>
diff --git a/web/components/answers/create-answer-panel.tsx b/web/components/answers/create-answer-panel.tsx
index af7f3cf2..35e04db2 100644
--- a/web/components/answers/create-answer-panel.tsx
+++ b/web/components/answers/create-answer-panel.tsx
@@ -38,6 +38,7 @@ export function CreateAnswerPanel(props: {
   const submitAnswer = async () => {
     if (canSubmit) {
       setIsSubmitting(true)
+
       const result = await createAnswer({
         contractId: contract.id,
         text,
@@ -50,7 +51,7 @@ export function CreateAnswerPanel(props: {
         setText('')
         setBetAmount(10)
         setAmountError(undefined)
-      }
+      } else setAmountError(result.message)
     }
   }
 
@@ -74,7 +75,7 @@ export function CreateAnswerPanel(props: {
   const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
 
   return (
-    <Col className="gap-4 p-4 bg-gray-50 rounded">
+    <Col className="gap-4 rounded bg-gray-50 p-4">
       <Col className="flex-1 gap-2">
         <div className="mb-1">Add your answer</div>
         <Textarea
@@ -88,14 +89,14 @@ export function CreateAnswerPanel(props: {
         <div />
         <Col
           className={clsx(
-            'sm:flex-row sm:items-end gap-4',
+            'gap-4 sm:flex-row sm:items-end',
             text ? 'justify-between' : 'self-end'
           )}
         >
           {text && (
             <>
-              <Col className="gap-2 mt-1">
-                <div className="text-gray-500 text-sm">Buy amount</div>
+              <Col className="mt-1 gap-2">
+                <div className="text-sm text-gray-500">Buy amount</div>
                 <AmountInput
                   amount={betAmount}
                   onChange={setBetAmount}
@@ -103,11 +104,11 @@ export function CreateAnswerPanel(props: {
                   setError={setAmountError}
                   minimumAmount={1}
                   disabled={isSubmitting}
-                  contractId={contract.id}
+                  contractIdForLoan={contract.id}
                 />
               </Col>
               <Col className="gap-3">
-                <Row className="justify-between items-center text-sm">
+                <Row className="items-center justify-between text-sm">
                   <div className="text-gray-500">Probability</div>
                   <Row>
                     <div>{formatPercent(0)}</div>
@@ -116,8 +117,8 @@ export function CreateAnswerPanel(props: {
                   </Row>
                 </Row>
 
-                <Row className="justify-between text-sm gap-2">
-                  <Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
+                <Row className="justify-between gap-2 text-sm">
+                  <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
                     <div>Payout if chosen</div>
                     <InfoTooltip
                       text={`Current payout for ${formatWithCommas(
@@ -125,7 +126,7 @@ export function CreateAnswerPanel(props: {
                       )} / ${formatWithCommas(shares)} shares`}
                     />
                   </Row>
-                  <Row className="flex-wrap justify-end items-end gap-2">
+                  <Row className="flex-wrap items-end justify-end gap-2">
                     <span className="whitespace-nowrap">
                       {formatMoney(currentPayout)}
                     </span>
diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx
index 5c3cd646..21c15a50 100644
--- a/web/components/bet-panel.tsx
+++ b/web/components/bet-panel.tsx
@@ -68,11 +68,6 @@ export function BetPanel(props: {
   async function submitBet() {
     if (!user || !betAmount) return
 
-    if (user.balance < betAmount) {
-      setError('Insufficient balance')
-      return
-    }
-
     setError(undefined)
     setIsSubmitting(true)
 
@@ -157,11 +152,11 @@ export function BetPanel(props: {
         setError={setError}
         disabled={isSubmitting}
         inputRef={inputRef}
-        contractId={contract.id}
+        contractIdForLoan={contract.id}
       />
 
-      <Col className="gap-3 mt-3 w-full">
-        <Row className="justify-between items-center text-sm">
+      <Col className="mt-3 w-full gap-3">
+        <Row className="items-center justify-between text-sm">
           <div className="text-gray-500">Probability</div>
           <Row>
             <div>{formatPercent(initialProb)}</div>
@@ -170,15 +165,15 @@ export function BetPanel(props: {
           </Row>
         </Row>
 
-        <Row className="justify-between items-start text-sm gap-2">
-          <Row className="flex-nowrap whitespace-nowrap items-center gap-2 text-gray-500">
+        <Row className="items-start justify-between gap-2 text-sm">
+          <Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
             <div>
               Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
             </div>
 
             {tooltip && <InfoTooltip text={tooltip} />}
           </Row>
-          <Row className="flex-wrap justify-end items-end gap-2">
+          <Row className="flex-wrap items-end justify-end gap-2">
             <span className="whitespace-nowrap">
               {formatMoney(currentPayout)}
             </span>
diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx
index 1c054b37..64090de1 100644
--- a/web/components/bets-list.tsx
+++ b/web/components/bets-list.tsx
@@ -127,8 +127,8 @@ export function BetsList(props: { user: User }) {
   const totalPortfolio = currentBetsValue + user.balance
 
   const totalPnl = totalPortfolio - user.totalDeposits
-  const totalProfit = (totalPnl / user.totalDeposits) * 100
-  const investedProfit =
+  const totalProfitPercent = (totalPnl / user.totalDeposits) * 100
+  const investedProfitPercent =
     ((currentBetsValue - currentInvestment) / currentInvestment) * 100
 
   return (
@@ -136,17 +136,17 @@ export function BetsList(props: { user: User }) {
       <Col className="mx-4 gap-4 sm:flex-row sm:justify-between md:mx-0">
         <Row className="gap-8">
           <Col>
-            <div className="text-sm text-gray-500">Invested</div>
+            <div className="text-sm text-gray-500">Investment value</div>
             <div className="text-lg">
-              {formatMoney(currentInvestment)}{' '}
-              <ProfitBadge profitPercent={investedProfit} />
+              {formatMoney(currentBetsValue)}{' '}
+              <ProfitBadge profitPercent={investedProfitPercent} />
             </div>
           </Col>
           <Col>
-            <div className="text-sm text-gray-500">Total portfolio</div>
+            <div className="text-sm text-gray-500">Total profit</div>
             <div className="text-lg">
-              {formatMoney(totalPortfolio)}{' '}
-              <ProfitBadge profitPercent={totalProfit} />
+              {formatMoney(totalPnl)}{' '}
+              <ProfitBadge profitPercent={totalProfitPercent} />
             </div>
           </Col>
         </Row>
@@ -527,6 +527,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
   const outcomeProb = getProbabilityAfterSale(contract, outcome, shares)
 
   const saleAmount = calculateSaleAmount(contract, bet)
+  const profit = saleAmount - bet.amount
 
   return (
     <ConfirmationButton
@@ -554,6 +555,8 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
       )}
 
       <div className="mt-2 mb-1 text-sm">
+        {profit > 0 ? 'Profit' : 'Loss'}: {formatMoney(profit).replace('-', '')}
+        <br />
         Market probability: {formatPercent(initialProb)} →{' '}
         {formatPercent(outcomeProb)}
       </div>
diff --git a/web/components/client-render.tsx b/web/components/client-render.tsx
new file mode 100644
index 00000000..a58c90ff
--- /dev/null
+++ b/web/components/client-render.tsx
@@ -0,0 +1,13 @@
+// Adapted from https://stackoverflow.com/a/50884055/1222351
+import { useEffect, useState } from 'react'
+
+export function ClientRender(props: { children: React.ReactNode }) {
+  const { children } = props
+
+  const [mounted, setMounted] = useState(false)
+  useEffect(() => {
+    setMounted(true)
+  }, [])
+
+  return mounted ? <>{children}</> : null
+}
diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx
index faafa2b3..b243a15d 100644
--- a/web/components/contract-feed.tsx
+++ b/web/components/contract-feed.tsx
@@ -25,7 +25,7 @@ import {
 import { useUser } from '../hooks/use-user'
 import { Linkify } from './linkify'
 import { Row } from './layout/row'
-import { createComment } from '../lib/firebase/comments'
+import { createComment, MAX_COMMENT_LENGTH } from '../lib/firebase/comments'
 import { useComments } from '../hooks/use-comments'
 import { formatMoney } from '../../common/util/format'
 import { ResolutionOrChance } from './contract-card'
@@ -136,6 +136,7 @@ function FeedBet(props: { activityItem: any; feedType: FeedType }) {
                 className="textarea textarea-bordered w-full"
                 placeholder="Add a comment..."
                 rows={3}
+                maxLength={MAX_COMMENT_LENGTH}
                 onKeyDown={(e) => {
                   if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
                     submitComment()
@@ -290,7 +291,10 @@ function TruncatedComment(props: {
   }
 
   return (
-    <div className="mt-2 whitespace-pre-line break-words text-gray-700">
+    <div
+      className="mt-2 whitespace-pre-line break-words text-gray-700"
+      style={{ fontSize: 15 }}
+    >
       <Linkify text={truncated} />
       {truncated != comment && (
         <SiteLink href={moreHref} className="text-indigo-700">
@@ -301,8 +305,11 @@ function TruncatedComment(props: {
   )
 }
 
-function FeedQuestion(props: { contract: Contract }) {
-  const { contract } = props
+function FeedQuestion(props: {
+  contract: Contract
+  showDescription?: boolean
+}) {
+  const { contract, showDescription } = props
   const { creatorName, creatorUsername, question, resolution, outcomeType } =
     contract
   const { truePool } = contractMetrics(contract)
@@ -337,22 +344,34 @@ function FeedQuestion(props: { contract: Contract }) {
             {closeMessage}
           </span>
         </div>
-        <Col className="mb-4 items-start justify-between gap-2 sm:flex-row sm:gap-4">
-          <SiteLink
-            href={contractPath(contract)}
-            className="text-lg text-indigo-700 sm:text-xl"
-          >
-            {question}
-          </SiteLink>
+        <Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
+          <Col>
+            <SiteLink
+              href={contractPath(contract)}
+              className="text-lg text-indigo-700 sm:text-xl"
+            >
+              {question}
+            </SiteLink>
+            {!showDescription && (
+              <SiteLink
+                href={contractPath(contract)}
+                className="self-end sm:self-start text-sm relative top-4"
+              >
+                <div className="text-gray-500 pb-1.5">See more...</div>
+              </SiteLink>
+            )}
+          </Col>
           {(isBinary || resolution) && (
             <ResolutionOrChance className="items-center" contract={contract} />
           )}
         </Col>
-        <TruncatedComment
-          comment={contract.description}
-          moreHref={contractPath(contract)}
-          shouldTruncate
-        />
+        {showDescription && (
+          <TruncatedComment
+            comment={contract.description}
+            moreHref={contractPath(contract)}
+            shouldTruncate
+          />
+        )}
       </div>
     </>
   )
@@ -684,6 +703,7 @@ type ActivityItem = {
     | 'close'
     | 'resolve'
     | 'expand'
+    | undefined
 }
 
 type FeedType =
@@ -694,64 +714,24 @@ type FeedType =
   // Grouped for a multi-category outcome
   | 'multi'
 
-export function ContractFeed(props: {
+function FeedItems(props: {
   contract: Contract
-  bets: Bet[]
-  comments: Comment[]
+  items: ActivityItem[]
   feedType: FeedType
+  setExpanded: (expanded: boolean) => void
   outcome?: string // Which multi-category outcome to filter
   betRowClassName?: string
 }) {
-  const { contract, feedType, outcome, betRowClassName } = props
-  const { id, outcomeType } = contract
+  const { contract, items, feedType, outcome, setExpanded, betRowClassName } =
+    props
+  const { outcomeType } = contract
   const isBinary = outcomeType === 'BINARY'
 
-  const [expanded, setExpanded] = useState(false)
-  const user = useUser()
-
-  let bets = useBets(contract.id) ?? props.bets
-  bets = isBinary
-    ? bets.filter((bet) => !bet.isAnte)
-    : bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
-
-  if (feedType === 'multi') {
-    bets = bets.filter((bet) => bet.outcome === outcome)
-  }
-
-  const comments = useComments(id) ?? props.comments
-
-  const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
-
-  const allItems = [
-    { type: 'start', id: 0 },
-    ...groupBets(bets, comments, groupWindow, user?.id),
-  ]
-  if (contract.closeTime && contract.closeTime <= Date.now()) {
-    allItems.push({ type: 'close', id: `${contract.closeTime}` })
-  }
-  if (contract.resolution) {
-    allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
-  }
-  if (feedType === 'multi') {
-    // Hack to add some more padding above the 'multi' feedType, by adding a null item
-    allItems.unshift({ type: '', id: -1 })
-  }
-
-  // If there are more than 5 items, only show the first, an expand item, and last 3
-  let items = allItems
-  if (!expanded && allItems.length > 5 && feedType == 'activity') {
-    items = [
-      allItems[0],
-      { type: 'expand', id: 'expand' },
-      ...allItems.slice(-3),
-    ]
-  }
-
   return (
     <div className="flow-root pr-2 md:pr-0">
-      <div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
+      <div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
         {items.map((activityItem, activityItemIdx) => (
-          <div key={activityItem.id} className="relative pb-8">
+          <div key={activityItem.id} className="relative pb-6">
             {activityItemIdx !== items.length - 1 ? (
               <span
                 className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
@@ -795,6 +775,117 @@ export function ContractFeed(props: {
   )
 }
 
+export function ContractFeed(props: {
+  contract: Contract
+  bets: Bet[]
+  comments: Comment[]
+  feedType: FeedType
+  outcome?: string // Which multi-category outcome to filter
+  betRowClassName?: string
+}) {
+  const { contract, feedType, outcome, betRowClassName } = props
+  const { id, outcomeType } = contract
+  const isBinary = outcomeType === 'BINARY'
+
+  const [expanded, setExpanded] = useState(false)
+  const user = useUser()
+
+  let bets = useBets(contract.id) ?? props.bets
+  bets = isBinary
+    ? bets.filter((bet) => !bet.isAnte)
+    : bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
+
+  if (feedType === 'multi') {
+    bets = bets.filter((bet) => bet.outcome === outcome)
+  }
+
+  const comments = useComments(id) ?? props.comments
+
+  const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
+
+  const allItems: ActivityItem[] = [
+    { type: 'start', id: '0' },
+    ...groupBets(bets, comments, groupWindow, user?.id),
+  ]
+  if (contract.closeTime && contract.closeTime <= Date.now()) {
+    allItems.push({ type: 'close', id: `${contract.closeTime}` })
+  }
+  if (contract.resolution) {
+    allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
+  }
+  if (feedType === 'multi') {
+    // Hack to add some more padding above the 'multi' feedType, by adding a null item
+    allItems.unshift({ type: undefined, id: '-1' })
+  }
+
+  // If there are more than 5 items, only show the first, an expand item, and last 3
+  let items = allItems
+  if (!expanded && allItems.length > 5 && feedType == 'activity') {
+    items = [
+      allItems[0],
+      { type: 'expand', id: 'expand' },
+      ...allItems.slice(-3),
+    ]
+  }
+
+  return (
+    <FeedItems
+      contract={contract}
+      items={items}
+      feedType={feedType}
+      setExpanded={setExpanded}
+      betRowClassName={betRowClassName}
+      outcome={outcome}
+    />
+  )
+}
+
+export function ContractActivityFeed(props: {
+  contract: Contract
+  bets: Bet[]
+  comments: Comment[]
+  betRowClassName?: string
+}) {
+  const { contract, betRowClassName, bets, comments } = props
+
+  const user = useUser()
+
+  bets.sort((b1, b2) => b1.createdTime - b2.createdTime)
+  comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
+
+  const allItems: ActivityItem[] = [
+    { type: 'start', id: '0' },
+    ...groupBets(bets, comments, DAY_IN_MS, user?.id),
+  ]
+  if (contract.closeTime && contract.closeTime <= Date.now()) {
+    allItems.push({ type: 'close', id: `${contract.closeTime}` })
+  }
+  if (contract.resolution) {
+    allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
+  }
+
+  // Remove all but last bet group.
+  const betGroups = allItems.filter((item) => item.type === 'betgroup')
+  const lastBetGroup = betGroups[betGroups.length - 1]
+  const filtered = allItems.filter(
+    (item) => item.type !== 'betgroup' || item.id === lastBetGroup?.id
+  )
+
+  // Only show the first item plus the last three items.
+  const items =
+    filtered.length > 3 ? [filtered[0], ...filtered.slice(-3)] : filtered
+
+  return (
+    <FeedItems
+      contract={contract}
+      items={items}
+      feedType="activity"
+      setExpanded={() => {}}
+      betRowClassName={betRowClassName}
+    />
+  )
+}
+
 export function ContractSummaryFeed(props: {
   contract: Contract
   betRowClassName?: string
@@ -808,7 +899,7 @@ export function ContractSummaryFeed(props: {
       <div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
         <div className="relative pb-8">
           <div className="relative flex items-start space-x-3">
-            <FeedQuestion contract={contract} />
+            <FeedQuestion contract={contract} showDescription />
           </div>
         </div>
       </div>
diff --git a/web/components/datetime-tooltip.tsx b/web/components/datetime-tooltip.tsx
index 6b8e5216..69c4521e 100644
--- a/web/components/datetime-tooltip.tsx
+++ b/web/components/datetime-tooltip.tsx
@@ -2,6 +2,7 @@ import dayjs from 'dayjs'
 import utc from 'dayjs/plugin/utc'
 import timezone from 'dayjs/plugin/timezone'
 import advanced from 'dayjs/plugin/advancedFormat'
+import { ClientRender } from './client-render'
 
 dayjs.extend(utc)
 dayjs.extend(timezone)
@@ -19,13 +20,15 @@ export function DateTimeTooltip(props: {
 
   return (
     <>
-      <span
-        className="tooltip hidden cursor-default sm:inline-block"
-        data-tip={toolTip}
-      >
-        {props.children}
-      </span>
-      <span className="sm:hidden whitespace-nowrap">{props.children}</span>
+      <ClientRender>
+        <span
+          className="tooltip hidden cursor-default sm:inline-block"
+          data-tip={toolTip}
+        >
+          {props.children}
+        </span>
+      </ClientRender>
+      <span className="whitespace-nowrap sm:hidden">{props.children}</span>
     </>
   )
 }
diff --git a/web/components/fast-fold-following.tsx b/web/components/fast-fold-following.tsx
index db0bd105..7a296464 100644
--- a/web/components/fast-fold-following.tsx
+++ b/web/components/fast-fold-following.tsx
@@ -32,7 +32,7 @@ function FollowFoldButton(props: {
       className={clsx(
         'rounded-full border-2 px-4 py-1 shadow-md',
         'cursor-pointer',
-        followed ? 'bg-gray-300 border-gray-300' : 'bg-white'
+        followed ? 'border-gray-300 bg-gray-300' : 'bg-white'
       )}
       onClick={onClick}
     >
@@ -88,7 +88,7 @@ export const FastFoldFollowing = (props: {
         user={user}
         followedFoldSlugs={followedFoldSlugs}
         folds={[
-          { name: 'Politics', slug: 'politics' },
+          { name: 'Russia/Ukraine', slug: 'russia-ukraine' },
           { name: 'Crypto', slug: 'crypto' },
           { name: 'Sports', slug: 'sports' },
           { name: 'Science', slug: 'science' },
diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx
index 3f9fa686..2a2d291c 100644
--- a/web/components/feed-create.tsx
+++ b/web/components/feed-create.tsx
@@ -1,5 +1,6 @@
+import { SparklesIcon, XIcon } from '@heroicons/react/solid'
 import { Avatar } from './avatar'
-import { useEffect, useRef, useState } from 'react'
+import { useRef, useState } from 'react'
 import { Spacer } from './layout/spacer'
 import { NewContract } from '../pages/create'
 import { firebaseLogin, User } from '../lib/firebase/users'
@@ -7,44 +8,51 @@ import { ContractsGrid } from './contracts-list'
 import { Contract } from '../../common/contract'
 import { Col } from './layout/col'
 import clsx from 'clsx'
+import { Row } from './layout/row'
 
 export function FeedPromo(props: { hotContracts: Contract[] }) {
   const { hotContracts } = props
 
   return (
     <>
-      <Col className="w-full bg-white p-6 sm:rounded-lg">
+      <Col className="m-6 mb-1 text-center sm:m-12">
         <h1 className="mt-4 text-4xl sm:mt-5 sm:text-6xl lg:mt-6 xl:text-6xl">
-          <div className="mb-2">Create your own</div>
-          <div className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text  font-bold text-transparent">
-            prediction markets
+          <div className="font-semibold sm:mb-2">
+            A{' '}
+            <span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
+              market{' '}
+            </span>
+            for
+          </div>
+          <div className="font-semibold">
+            every{' '}
+            <span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
+              prediction
+            </span>
           </div>
         </h1>
         <Spacer h={6} />
         <div className="mb-4 text-gray-500">
-          Find prediction markets run by your favorite creators, or make your
-          own.
+          Find prediction markets on any topic imaginable. Or create your own!
           <br />
-          Sign up to get M$ 1,000 for free and start trading!
+          Sign up to get M$ 1,000 and start trading.
           <br />
         </div>
         <Spacer h={6} />
         <button
-          className="btn btn-lg self-center border-none bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
+          className="btn btn-lg self-center border-none bg-gradient-to-r from-teal-500 to-green-500  normal-case hover:from-teal-600 hover:to-green-600"
           onClick={firebaseLogin}
         >
-          Sign up now
+          Sign up for free
         </button>{' '}
       </Col>
 
-      <Spacer h={6} />
-      {/* 
-      <TagsList
-        className="mt-2"
-        tags={['#politics', '#crypto', '#covid', '#sports', '#meta']}
-      />
-      <Spacer h={6} /> */}
+      <Spacer h={12} />
 
+      <Row className="m-4 mb-6 items-center gap-1 text-xl font-semibold text-gray-800">
+        <SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
+        Trending today
+      </Row>
       <ContractsGrid
         contracts={hotContracts?.slice(0, 10) || []}
         showHotVolume
@@ -61,7 +69,8 @@ export default function FeedCreate(props: {
 }) {
   const { user, tag, className } = props
   const [question, setQuestion] = useState('')
-  const [focused, setFocused] = useState(false)
+  const [isExpanded, setIsExpanded] = useState(false)
+  const inputRef = useRef<HTMLTextAreaElement | null>()
 
   const placeholders = [
     'Will anyone I know get engaged this year?',
@@ -78,60 +87,60 @@ export default function FeedCreate(props: {
   )
   const placeholder = props.placeholder ?? `e.g. ${placeholders[randIndex]}`
 
-  const panelRef = useRef<HTMLElement | null>()
-  const inputRef = useRef<HTMLTextAreaElement | null>()
-
-  useEffect(() => {
-    const onClick = () => {
-      if (
-        panelRef.current &&
-        document.activeElement &&
-        !panelRef.current.contains(document.activeElement)
-      )
-        setFocused(false)
-    }
-    window.addEventListener('click', onClick)
-    return () => window.removeEventListener('click', onClick)
-  })
-
   return (
     <div
       className={clsx(
-        'mt-2 w-full rounded bg-white p-4 shadow-md',
-        question || focused ? 'ring-2 ring-indigo-300' : '',
+        'mt-2 w-full rounded bg-white p-4 shadow-md cursor-text',
+        isExpanded ? 'ring-2 ring-indigo-300' : '',
         className
       )}
-      onClick={() => !focused && inputRef.current?.focus()}
-      ref={(elem) => (panelRef.current = elem)}
+      onClick={() => {
+        !isExpanded && inputRef.current?.focus()
+      }}
     >
       <div className="relative flex items-start space-x-3">
         <Avatar username={user?.username} avatarUrl={user?.avatarUrl} noLink />
 
         <div className="min-w-0 flex-1">
-          {/* TODO: Show focus, for accessibility */}
-          <div>
+          <Row className="justify-between">
             <p className="my-0.5 text-sm">Ask a question... </p>
-          </div>
+            {isExpanded && (
+              <button
+                className="btn btn-xs btn-circle btn-ghost rounded"
+                onClick={() => setIsExpanded(false)}
+              >
+                <XIcon
+                  className="mx-auto h-6 w-6 text-gray-500"
+                  aria-hidden="true"
+                />
+              </button>
+            )}
+          </Row>
           <textarea
             ref={inputRef as any}
-            className="w-full resize-none appearance-none border-transparent bg-transparent p-0 text-lg text-indigo-700 placeholder:text-gray-400 focus:border-transparent focus:ring-transparent sm:text-xl"
+            className={clsx(
+              'w-full resize-none appearance-none border-transparent bg-transparent p-0 text-indigo-700 placeholder:text-gray-400 focus:border-transparent focus:ring-transparent',
+              question && 'text-lg sm:text-xl',
+              !question && 'text-base sm:text-lg'
+            )}
             placeholder={placeholder}
             value={question}
+            rows={question.length > 68 ? 4 : 2}
             onClick={(e) => e.stopPropagation()}
             onChange={(e) => setQuestion(e.target.value.replace('\n', ''))}
-            onFocus={() => setFocused(true)}
+            onFocus={() => setIsExpanded(true)}
           />
         </div>
       </div>
 
       {/* Hide component instead of deleting, so edits to NewContract don't get lost */}
-      <div className={question || focused ? '' : 'hidden'}>
+      <div className={isExpanded ? '' : 'hidden'}>
         <NewContract question={question} tag={tag} />
       </div>
 
       {/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
-      {!(question || focused) && (
-        <div className="flex justify-end">
+      {!isExpanded && (
+        <div className="flex justify-end sm:-mt-4">
           <button className="btn btn-sm" disabled>
             Create Market
           </button>
diff --git a/web/hooks/use-find-active-contracts.ts b/web/hooks/use-find-active-contracts.ts
index f8aa5627..2a6a3b47 100644
--- a/web/hooks/use-find-active-contracts.ts
+++ b/web/hooks/use-find-active-contracts.ts
@@ -4,11 +4,11 @@ import { useMemo, useRef } from 'react'
 import { Fold } from '../../common/fold'
 import { User } from '../../common/user'
 import { filterDefined } from '../../common/util/array'
-import { Bet, getRecentBets } from '../lib/firebase/bets'
+import { Bet } from '../lib/firebase/bets'
 import { Comment, getRecentComments } from '../lib/firebase/comments'
 import { Contract, getActiveContracts } from '../lib/firebase/contracts'
 import { listAllFolds } from '../lib/firebase/folds'
-import { findActiveContracts } from '../pages/activity'
+import { findActiveContracts } from '../components/activity-feed'
 import { useInactiveContracts } from './use-contracts'
 import { useFollowedFolds } from './use-fold'
 import { useUserBetContracts } from './use-user-bets'
@@ -20,12 +20,9 @@ export const getAllContractInfo = async () => {
     listAllFolds().catch(() => []),
   ])
 
-  const [recentBets, recentComments] = await Promise.all([
-    getRecentBets(),
-    getRecentComments(),
-  ])
+  const recentComments = await getRecentComments()
 
-  return { contracts, recentBets, recentComments, folds }
+  return { contracts, recentComments, folds }
 }
 
 export const useFilterYourContracts = (
diff --git a/web/lib/firebase/bets.ts b/web/lib/firebase/bets.ts
index f03b293b..4056e114 100644
--- a/web/lib/firebase/bets.ts
+++ b/web/lib/firebase/bets.ts
@@ -103,3 +103,24 @@ export function withoutAnteBets(contract: Contract, bets?: Bet[]) {
 
   return bets?.filter((bet) => !bet.isAnte) ?? []
 }
+
+const getBetsQuery = (startTime: number, endTime: number) =>
+  query(
+    collectionGroup(db, 'bets'),
+    where('createdTime', '>=', startTime),
+    where('createdTime', '<', endTime),
+    orderBy('createdTime', 'asc')
+  )
+
+export async function getDailyBets(startTime: number, numberOfDays: number) {
+  const query = getBetsQuery(startTime, startTime + DAY_IN_MS * numberOfDays)
+  const bets = await getValues<Bet>(query)
+
+  const betsByDay = _.range(0, numberOfDays).map(() => [] as Bet[])
+  for (const bet of bets) {
+    const dayIndex = Math.floor((bet.createdTime - startTime) / DAY_IN_MS)
+    betsByDay[dayIndex].push(bet)
+  }
+
+  return betsByDay
+}
diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts
index 34e4bcfe..a6b9d9ea 100644
--- a/web/lib/firebase/comments.ts
+++ b/web/lib/firebase/comments.ts
@@ -7,6 +7,7 @@ import {
   where,
   orderBy,
 } from 'firebase/firestore'
+import _ from 'lodash'
 
 import { getValues, listenForValues } from './utils'
 import { db } from './init'
@@ -14,6 +15,8 @@ import { User } from '../../../common/user'
 import { Comment } from '../../../common/comment'
 export type { Comment }
 
+export const MAX_COMMENT_LENGTH = 10000
+
 export async function createComment(
   contractId: string,
   betId: string,
@@ -27,7 +30,7 @@ export async function createComment(
     contractId,
     betId,
     userId: commenter.id,
-    text,
+    text: text.slice(0, MAX_COMMENT_LENGTH),
     createdTime: Date.now(),
     userName: commenter.name,
     userUsername: commenter.username,
@@ -87,3 +90,30 @@ export function listenForRecentComments(
 ) {
   return listenForValues<Comment>(recentCommentsQuery, setComments)
 }
+
+const getCommentsQuery = (startTime: number, endTime: number) =>
+  query(
+    collectionGroup(db, 'comments'),
+    where('createdTime', '>=', startTime),
+    where('createdTime', '<', endTime),
+    orderBy('createdTime', 'asc')
+  )
+
+export async function getDailyComments(
+  startTime: number,
+  numberOfDays: number
+) {
+  const query = getCommentsQuery(
+    startTime,
+    startTime + DAY_IN_MS * numberOfDays
+  )
+  const comments = await getValues<Comment>(query)
+
+  const commentsByDay = _.range(0, numberOfDays).map(() => [] as Comment[])
+  for (const comment of comments) {
+    const dayIndex = Math.floor((comment.createdTime - startTime) / DAY_IN_MS)
+    commentsByDay[dayIndex].push(comment)
+  }
+
+  return commentsByDay
+}
diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts
index e6d2446d..7227f4fc 100644
--- a/web/lib/firebase/contracts.ts
+++ b/web/lib/firebase/contracts.ts
@@ -213,3 +213,32 @@ export async function getClosingSoonContracts() {
     (contract) => contract.closeTime
   )
 }
+
+const getContractsQuery = (startTime: number, endTime: number) =>
+  query(
+    collection(db, 'contracts'),
+    where('createdTime', '>=', startTime),
+    where('createdTime', '<', endTime),
+    orderBy('createdTime', 'asc')
+  )
+
+const DAY_IN_MS = 24 * 60 * 60 * 1000
+
+export async function getDailyContracts(
+  startTime: number,
+  numberOfDays: number
+) {
+  const query = getContractsQuery(
+    startTime,
+    startTime + DAY_IN_MS * numberOfDays
+  )
+  const contracts = await getValues<Contract>(query)
+
+  const contractsByDay = _.range(0, numberOfDays).map(() => [] as Contract[])
+  for (const contract of contracts) {
+    const dayIndex = Math.floor((contract.createdTime - startTime) / DAY_IN_MS)
+    contractsByDay[dayIndex].push(contract)
+  }
+
+  return contractsByDay
+}
diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx
index 1020816d..38b7cd3c 100644
--- a/web/pages/[username]/[contractSlug].tsx
+++ b/web/pages/[username]/[contractSlug].tsx
@@ -116,7 +116,7 @@ export default function ContractPage(props: {
       )}
 
       <Col className="w-full justify-between md:flex-row">
-        <div className="flex-[3] rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8">
+        <div className="flex-1 rounded border-0 border-gray-100 bg-white px-2 py-6 md:px-6 md:py-8">
           <ContractOverview
             contract={contract}
             bets={bets ?? []}
@@ -143,7 +143,7 @@ export default function ContractPage(props: {
           <>
             <div className="md:ml-6" />
 
-            <Col className="flex-1">
+            <Col className="md:w-[310px] flex-shrink-0">
               {allowTrade && (
                 <BetPanel className="hidden lg:flex" contract={contract} />
               )}
diff --git a/web/pages/add-funds.tsx b/web/pages/add-funds.tsx
index 2ecf6317..339be265 100644
--- a/web/pages/add-funds.tsx
+++ b/web/pages/add-funds.tsx
@@ -19,10 +19,10 @@ export default function AddFundsPage() {
       <SEO title="Add funds" description="Add funds" url="/add-funds" />
 
       <Col className="items-center">
-        <Col className="bg-white rounded sm:shadow-md p-4 py-8 sm:p-8 h-full">
+        <Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
           <Title className="!mt-0" text="Get Manifold Dollars" />
           <img
-            className="mb-6 block self-center -scale-x-100"
+            className="mb-6 block -scale-x-100 self-center"
             src="/stylized-crane-black.png"
             width={200}
             height={200}
diff --git a/web/pages/analytics.tsx b/web/pages/analytics.tsx
index 7f905a46..268c0d51 100644
--- a/web/pages/analytics.tsx
+++ b/web/pages/analytics.tsx
@@ -1,17 +1,121 @@
+import dayjs from 'dayjs'
+import _ from 'lodash'
+import { DailyCountChart } from '../components/analytics/charts'
+import { Col } from '../components/layout/col'
+import { Spacer } from '../components/layout/spacer'
 import { Page } from '../components/page'
+import { Title } from '../components/title'
+import { getDailyBets } from '../lib/firebase/bets'
+import { getDailyComments } from '../lib/firebase/comments'
+import { getDailyContracts } from '../lib/firebase/contracts'
 
-export default function Analytics() {
-  // Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit
+export async function getStaticProps() {
+  const numberOfDays = 80
+  const today = dayjs(dayjs().format('YYYY-MM-DD'))
+  const startDate = today.subtract(numberOfDays, 'day')
+
+  const [dailyBets, dailyContracts, dailyComments] = await Promise.all([
+    getDailyBets(startDate.valueOf(), numberOfDays),
+    getDailyContracts(startDate.valueOf(), numberOfDays),
+    getDailyComments(startDate.valueOf(), numberOfDays),
+  ])
+
+  const dailyBetCounts = dailyBets.map((bets) => bets.length)
+  const dailyContractCounts = dailyContracts.map(
+    (contracts) => contracts.length
+  )
+  const dailyCommentCounts = dailyComments.map((comments) => comments.length)
+
+  const dailyActiveUsers = _.zip(dailyContracts, dailyBets, dailyComments).map(
+    ([contracts, bets, comments]) => {
+      const creatorIds = (contracts ?? []).map((c) => c.creatorId)
+      const betUserIds = (bets ?? []).map((bet) => bet.userId)
+      const commentUserIds = (comments ?? []).map((comment) => comment.userId)
+      return _.uniq([...creatorIds, ...betUserIds, commentUserIds]).length
+    }
+  )
+
+  return {
+    props: {
+      startDate: startDate.valueOf(),
+      dailyActiveUsers,
+      dailyBetCounts,
+      dailyContractCounts,
+      dailyCommentCounts,
+    },
+    revalidate: 12 * 60 * 60, // regenerate after half a day
+  }
+}
+
+export default function Analytics(props: {
+  startDate: number
+  dailyActiveUsers: number[]
+  dailyBetCounts: number[]
+  dailyContractCounts: number[]
+  dailyCommentCounts: number[]
+}) {
   return (
     <Page>
-      <iframe
-        className="w-full"
-        height={2200}
-        src="https://datastudio.google.com/embed/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3"
-        frameBorder="0"
-        style={{ border: 0 }}
-        allowFullScreen
-      ></iframe>
+      <CustomAnalytics {...props} />
+      <Spacer h={8} />
+      <FirebaseAnalytics />
     </Page>
   )
 }
+
+function CustomAnalytics(props: {
+  startDate: number
+  dailyActiveUsers: number[]
+  dailyBetCounts: number[]
+  dailyContractCounts: number[]
+  dailyCommentCounts: number[]
+}) {
+  const {
+    startDate,
+    dailyActiveUsers,
+    dailyBetCounts,
+    dailyContractCounts,
+    dailyCommentCounts,
+  } = props
+  return (
+    <Col>
+      <Title text="Active users" />
+      <DailyCountChart dailyCounts={dailyActiveUsers} startDate={startDate} />
+
+      <Title text="Bets count" />
+      <DailyCountChart
+        dailyCounts={dailyBetCounts}
+        startDate={startDate}
+        small
+      />
+
+      <Title text="Markets count" />
+      <DailyCountChart
+        dailyCounts={dailyContractCounts}
+        startDate={startDate}
+        small
+      />
+
+      <Title text="Comments count" />
+      <DailyCountChart
+        dailyCounts={dailyCommentCounts}
+        startDate={startDate}
+        small
+      />
+    </Col>
+  )
+}
+
+function FirebaseAnalytics() {
+  // Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit
+  return (
+    <iframe
+      className="w-full"
+      height={2200}
+      src="https://datastudio.google.com/embed/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3"
+      frameBorder="0"
+      style={{ border: 0 }}
+      allowFullScreen
+    />
+  )
+}
diff --git a/web/pages/create.tsx b/web/pages/create.tsx
index 76afa8d2..5e8830e0 100644
--- a/web/pages/create.tsx
+++ b/web/pages/create.tsx
@@ -137,7 +137,7 @@ export function NewContract(props: { question: string; tag?: string }) {
         <span className="mb-1">Answer type</span>
       </label>
       <Row className="form-control gap-2">
-        <label className="cursor-pointer label gap-2">
+        <label className="label cursor-pointer gap-2">
           <input
             className="radio"
             type="radio"
@@ -149,7 +149,7 @@ export function NewContract(props: { question: string; tag?: string }) {
           <span className="label-text">Yes / No</span>
         </label>
 
-        <label className="cursor-pointer label gap-2">
+        <label className="label cursor-pointer gap-2">
           <input
             className="radio"
             type="radio"
@@ -248,7 +248,7 @@ export function NewContract(props: { question: string; tag?: string }) {
           error={anteError}
           setError={setAnteError}
           disabled={isSubmitting}
-          contractId={undefined}
+          contractIdForLoan={undefined}
         />
       </div>
 
diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx
index 1d77444b..757fe325 100644
--- a/web/pages/fold/[...slugs]/index.tsx
+++ b/web/pages/fold/[...slugs]/index.tsx
@@ -12,7 +12,10 @@ import {
   getFoldBySlug,
   getFoldContracts,
 } from '../../../lib/firebase/folds'
-import { ActivityFeed, findActiveContracts } from '../../activity'
+import {
+  ActivityFeed,
+  findActiveContracts,
+} from '../../../components/activity-feed'
 import { TagsList } from '../../../components/tags-list'
 import { Row } from '../../../components/layout/row'
 import { UserLink } from '../../../components/user-page'
@@ -36,6 +39,9 @@ import { SEO } from '../../../components/SEO'
 import { useTaggedContracts } from '../../../hooks/use-contracts'
 import { Linkify } from '../../../components/linkify'
 import { filterDefined } from '../../../../common/util/array'
+import { useRecentBets } from '../../../hooks/use-bets'
+import { useRecentComments } from '../../../hooks/use-comments'
+import { LoadingIndicator } from '../../../components/loading-indicator'
 
 export async function getStaticProps(props: { params: { slugs: string[] } }) {
   const { slugs } = props.params
@@ -48,7 +54,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
   const bets = await Promise.all(
     contracts.map((contract) => listAllBets(contract.id))
   )
-  const betsByContract = _.fromPairs(contracts.map((c, i) => [c.id, bets[i]]))
 
   let activeContracts = findActiveContracts(contracts, [], _.flatten(bets))
   const [resolved, unresolved] = _.partition(
@@ -57,10 +62,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
   )
   activeContracts = [...unresolved, ...resolved]
 
-  const activeContractBets = activeContracts.map(
-    (contract) => betsByContract[contract.id] ?? []
-  )
-
   const creatorScores = scoreCreators(contracts, bets)
   const traderScores = scoreTraders(contracts, bets)
   const [topCreators, topTraders] = await Promise.all([
@@ -76,8 +77,6 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
       curator,
       contracts,
       activeContracts,
-      activeContractBets,
-      activeContractComments: activeContracts.map(() => []),
       traderScores,
       topTraders,
       creatorScores,
@@ -117,15 +116,8 @@ export default function FoldPage(props: {
   creatorScores: { [userId: string]: number }
   topCreators: User[]
 }) {
-  const {
-    curator,
-    activeContractBets,
-    activeContractComments,
-    traderScores,
-    topTraders,
-    creatorScores,
-    topCreators,
-  } = props
+  const { curator, traderScores, topTraders, creatorScores, topCreators } =
+    props
 
   const router = useRouter()
   const { slugs } = router.query as { slugs: string[] }
@@ -151,6 +143,9 @@ export default function FoldPage(props: {
     props.activeContracts.map((contract) => contractsMap[contract.id])
   )
 
+  const recentBets = useRecentBets()
+  const recentComments = useRecentComments()
+
   if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
     return <Custom404 />
   }
@@ -233,19 +228,24 @@ export default function FoldPage(props: {
               />
             )}
             {page === 'activity' ? (
-              <>
-                <ActivityFeed
-                  contracts={activeContracts}
-                  contractBets={activeContractBets}
-                  contractComments={activeContractComments}
-                />
-                {activeContracts.length === 0 && (
-                  <div className="mx-2 mt-4 text-gray-500 lg:mx-0">
-                    No activity from matching markets.{' '}
-                    {isCurator && 'Try editing to add more tags!'}
-                  </div>
-                )}
-              </>
+              recentBets && recentComments ? (
+                <>
+                  <ActivityFeed
+                    contracts={activeContracts}
+                    recentBets={recentBets ?? []}
+                    recentComments={recentComments ?? []}
+                    loadBetAndCommentHistory
+                  />
+                  {activeContracts.length === 0 && (
+                    <div className="mx-2 mt-4 text-gray-500 lg:mx-0">
+                      No activity from matching markets.{' '}
+                      {isCurator && 'Try editing to add more tags!'}
+                    </div>
+                  )}
+                </>
+              ) : (
+                <LoadingIndicator className="mt-4" />
+              )
             ) : (
               <SearchableGrid
                 contracts={contracts}
diff --git a/web/pages/home.tsx b/web/pages/home.tsx
index d7afdad1..548f1057 100644
--- a/web/pages/home.tsx
+++ b/web/pages/home.tsx
@@ -6,9 +6,8 @@ import _ from 'lodash'
 
 import { Contract } from '../lib/firebase/contracts'
 import { Page } from '../components/page'
-import { ActivityFeed, SummaryActivityFeed } from './activity'
+import { ActivityFeed, SummaryActivityFeed } from '../components/activity-feed'
 import { Comment } from '../lib/firebase/comments'
-import { Bet } from '../lib/firebase/bets'
 import FeedCreate from '../components/feed-create'
 import { Spacer } from '../components/layout/spacer'
 import { Col } from '../components/layout/col'
@@ -23,8 +22,9 @@ import {
   useFilterYourContracts,
   useFindActiveContracts,
 } from '../hooks/use-find-active-contracts'
-import { useGetRecentBets } from '../hooks/use-bets'
+import { useGetRecentBets, useRecentBets } from '../hooks/use-bets'
 import { useActiveContracts } from '../hooks/use-contracts'
+import { useRecentComments } from '../hooks/use-comments'
 
 export async function getStaticProps() {
   const contractInfo = await getAllContractInfo()
@@ -38,10 +38,9 @@ export async function getStaticProps() {
 const Home = (props: {
   contracts: Contract[]
   folds: Fold[]
-  recentBets: Bet[]
   recentComments: Comment[]
 }) => {
-  const { folds, recentComments } = props
+  const { folds } = props
   const user = useUser()
 
   const contracts = useActiveContracts() ?? props.contracts
@@ -51,13 +50,15 @@ const Home = (props: {
     contracts
   )
 
-  const recentBets = useGetRecentBets()
-  const { activeContracts, activeBets, activeComments } =
-    useFindActiveContracts({
-      contracts: yourContracts,
-      recentBets: recentBets ?? [],
-      recentComments,
-    })
+  const initialRecentBets = useGetRecentBets()
+  const recentBets = useRecentBets() ?? initialRecentBets
+  const recentComments = useRecentComments() ?? props.recentComments
+
+  const { activeContracts } = useFindActiveContracts({
+    contracts: yourContracts,
+    recentBets: initialRecentBets ?? [],
+    recentComments: props.recentComments,
+  })
 
   const exploreContracts = useExploreContracts()
 
@@ -71,7 +72,7 @@ const Home = (props: {
   return (
     <Page assertUser="signed-in">
       <Col className="items-center">
-        <Col className="w-full max-w-3xl">
+        <Col className="w-full max-w-[700px]">
           <FeedCreate user={user ?? undefined} />
           <Spacer h={6} />
 
@@ -85,7 +86,7 @@ const Home = (props: {
 
           <Spacer h={5} />
 
-          <Col className="mx-3 mb-3 gap-2 text-sm text-gray-800 sm:flex-row">
+          <Col className="mb-3 gap-2 text-sm text-gray-800 sm:flex-row">
             <Row className="gap-2">
               <div className="tabs">
                 <div
@@ -116,8 +117,8 @@ const Home = (props: {
             (recentBets ? (
               <ActivityFeed
                 contracts={activeContracts}
-                contractBets={activeBets}
-                contractComments={activeComments}
+                recentBets={recentBets}
+                recentComments={recentComments}
               />
             ) : (
               <LoadingIndicator className="mt-4" />
diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx
index c3d42d6d..36d210ef 100644
--- a/web/pages/make-predictions.tsx
+++ b/web/pages/make-predictions.tsx
@@ -248,7 +248,7 @@ ${TEST_VALUE}
             error={anteError}
             setError={setAnteError}
             disabled={isSubmitting}
-            contractId={undefined}
+            contractIdForLoan={undefined}
           />
         </div>