Switch to a Michaelis-Menten saturating curve. search bar filtering. mobile improvements
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m50s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m50s
This commit is contained in:
@@ -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
@@ -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'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) · 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'll get a warning on the
|
||||
home page if any of your positions are at risk. Research it again to reactivate it.
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user