Switch to a Michaelis-Menten saturating curve. search bar filtering. mobile improvements
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m50s

This commit is contained in:
2026-03-24 01:44:11 -04:00
parent 100f149c53
commit 15378c1eec
5 changed files with 56 additions and 24 deletions
+13 -8
View File
@@ -152,21 +152,26 @@ All variables are documented in `.env.example`. Key ones:
## Pricing Formula
Prices follow a **saturating curve** (Michaelis-Menten) so that viral hashtags don't produce runaway prices:
```
price = max($0.25, round(postsPerHour × $0.25, 2))
price = max($0.25, round((base × pph) / (1 + k × pph), 2))
```
Examples:
`k` is derived from two anchor points: floor price `$0.25` and a target of `$250` at 3,600 PPH (one post per second).
| Posts/hr | Price |
|---|---|
| 1 | $0.25 |
| 10 | $2.50 |
| 100 | $25.00 |
| 1,000 | $250.00 |
| 12,000 (e.g. #happynewyear at midnight) | $3,000.00 |
| 1 | ~$0.25 |
| 10 | ~$2.48 |
| 100 | ~$23.32 |
| 1,000 | ~$145 |
| 3,600 (one post/sec) | ~$250 |
| ∞ (theoretical) | ~$346 (asymptote) |
**Burst handling:** when all fetched posts share a very tight timestamp window the worker paginates up to `MAX_PAGES_PER_HASHTAG` pages to get a realistic count before the span grows to > 5 minutes.
At low activity the curve is approximately linear (≈ $0.25 per post/hr). At high activity it flattens, preventing a single trending hashtag from dwarfing the entire market.
**Burst handling:** the worker fetches up to `MAX_PAGES_PER_HASHTAG` pages of Mastodon results and uses only posts within the most recent hour when calculating PPH. If the fetched results are exhausted before covering a full hour, PPH is extrapolated from the covered window.
---
+14 -4
View File
@@ -71,12 +71,22 @@ export default function AboutPage() {
<Section title="How Prices Work" icon={Coins}>
<div className="space-y-2 text-sm text-slate-300">
<p>
Every hashtag has a price calculated from its post rate on Mastodon:
Every hashtag has a price derived from its posts-per-hour rate on Mastodon using a
{' '}<span className="text-white font-medium">saturating curve</span> prices rise quickly at
low activity and flatten at high activity so a single viral tag can&apos;t dominate the market.
</p>
<div className="bg-surface-card border border-surface-border rounded-lg px-4 py-3 font-mono text-center text-indigo-300">
price = max($0.25, posts_per_hour × $0.25)
<div className="bg-surface-card border border-surface-border rounded-lg px-4 py-3 font-mono text-center text-indigo-300 text-xs">
price = (0.25 × pph) / (1 + k × pph) &nbsp;·&nbsp; floor $0.25
</div>
<p>
<div className="grid grid-cols-2 gap-x-4 text-xs mt-1">
<span className="text-slate-400">1 post / hr</span><span>~$0.25</span>
<span className="text-slate-400">10 posts / hr</span><span>~$2.48</span>
<span className="text-slate-400">100 posts / hr</span><span>~$23</span>
<span className="text-slate-400">1,000 posts / hr</span><span>~$145</span>
<span className="text-slate-400">3,600 posts / hr (1/sec)</span><span>~$250</span>
<span className="text-slate-400"> (asymptote)</span><span>~$346</span>
</div>
<p className="text-xs text-slate-400">
Prices update on a regular cycle. A hashtag that goes completely quiet for long enough will be
automatically <span className="text-orange-400">deactivated</span> you&apos;ll get a warning on the
home page if any of your positions are at risk. Research it again to reactivate it.
+4 -1
View File
@@ -85,7 +85,10 @@ export default async function AdminStocksPage({ searchParams }: Props) {
{hashtags.map((h) => (
<tr key={h.id} className="hover:bg-surface-hover">
<td className="px-4 py-3">
<a href={`/hashtag/${h.tag}`} className="hover:text-indigo-300">
<a
href={`/hashtag/${h.tag}`}
className={`hover:text-indigo-300 ${!h.isActive && !h.isBanned ? 'text-slate-500' : ''}`}
>
#{h.displayTag}
</a>
</td>
+19 -9
View File
@@ -1,17 +1,27 @@
/**
* Converts posts-per-hour to a share price.
* Converts posts-per-hour to a share price using a saturating (Michaelis-Menten) curve.
*
* Linear scale: $0.25 per post/hour, minimum $0.25.
* Examples:
* 1 post/hr → $0.25
* 10 posts/hr → $2.50
* 100 → $25.00
* 1000 → $250.00
* 12 000 (viral #happynewyear) → $3 000
* Formula: price = base * pph / (1 + k * pph)
* where k is chosen so the curve hits $250 at 3 600 PPH.
*
* Anchor points:
* 1 post/hr → ~$0.25
* 10 posts/hr~$2.48
* 100 posts/hr → ~$23.32
* 1 000 → ~$145
* 3 600 (viral) → $250.00 (design target)
* Asymptote → ~$346
*
* Floor: $0.25 (matches zero-PPH floor price).
*/
export function calcPrice(postsPerHour: number): number {
if (postsPerHour <= 0) return 0.25
return Math.max(0.25, Math.round(postsPerHour * 0.25 * 100) / 100)
const base = 0.25 // The base price at low volumes (1 PPH)
const anchor = 3600 // PPH at which we want the target price (1 PPS)
const target = 250 // price at the anchor PPH
const k = ((base * anchor / target) - 1) / anchor
const price = base * postsPerHour / (1 + k * postsPerHour)
return Math.max(0.25, Math.round(price * 100) / 100)
}
/**
+6 -2
View File
@@ -41,7 +41,11 @@ export function pnlColor(value: number): string {
return 'text-slate-400'
}
/** Normalize a hashtag: lowercase, strip leading #, trim whitespace */
/** Normalize a hashtag: lowercase, strip leading #, remove all whitespace and
* any character that isn't a letter, digit, or underscore. */
export function normalizeTag(raw: string): string {
return raw.trim().replace(/^#+/, '').toLowerCase()
return raw
.replace(/^#+/, '') // strip leading #
.replace(/[\s]/g, '') // remove all whitespace
.toLowerCase()
}