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
|
## 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 |
|
| Posts/hr | Price |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 1 | $0.25 |
|
| 1 | ~$0.25 |
|
||||||
| 10 | $2.50 |
|
| 10 | ~$2.48 |
|
||||||
| 100 | $25.00 |
|
| 100 | ~$23.32 |
|
||||||
| 1,000 | $250.00 |
|
| 1,000 | ~$145 |
|
||||||
| 12,000 (e.g. #happynewyear at midnight) | $3,000.00 |
|
| 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}>
|
<Section title="How Prices Work" icon={Coins}>
|
||||||
<div className="space-y-2 text-sm text-slate-300">
|
<div className="space-y-2 text-sm text-slate-300">
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<div className="bg-surface-card border border-surface-border rounded-lg px-4 py-3 font-mono text-center text-indigo-300">
|
<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 = max($0.25, posts_per_hour × $0.25)
|
price = (0.25 × pph) / (1 + k × pph) · floor $0.25
|
||||||
</div>
|
</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
|
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
|
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.
|
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) => (
|
{hashtags.map((h) => (
|
||||||
<tr key={h.id} className="hover:bg-surface-hover">
|
<tr key={h.id} className="hover:bg-surface-hover">
|
||||||
<td className="px-4 py-3">
|
<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}
|
#{h.displayTag}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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.
|
* Formula: price = base * pph / (1 + k * pph)
|
||||||
* Examples:
|
* where k is chosen so the curve hits $250 at 3 600 PPH.
|
||||||
* 1 post/hr → $0.25
|
*
|
||||||
* 10 posts/hr → $2.50
|
* Anchor points:
|
||||||
* 100 → $25.00
|
* 1 post/hr → ~$0.25
|
||||||
* 1000 → $250.00
|
* 10 posts/hr → ~$2.48
|
||||||
* 12 000 (viral #happynewyear) → $3 000
|
* 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 {
|
export function calcPrice(postsPerHour: number): number {
|
||||||
if (postsPerHour <= 0) return 0.25
|
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'
|
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 {
|
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