SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

web: page Access & limits (/limits) — plateforme gratuite, 3 niveaux (keyless · compte gratuit · usage élevé sur demande), CTA compte gratuit + courriel contact@spboucher.ai, FAQ, incitations ; nav « Limits », plus aucune notion de tarification (shim temporaire src/pages/pricing jusqu'au routage /limits dans App.jsx)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 20 days ago (Sep 5, 2026) parent 2079d13

6 changed files +168 −8

modified hfmarketdata/web/src/app/Layout.jsx +2 −2
@@ -24,7 +24,7 @@ function useSearchShortcut(open) {
24 24
25 25 export const NAV = [
26 26 ['/docs', 'Docs'], ['/playground', 'Playground'], ['/integrations', 'Integrations'],
27 − ['/pricing', 'Pricing / Limits'], ['/status', 'Status'],
27 + ['/limits', 'Limits'], ['/status', 'Status'],
28 28 ]
29 29
30 30 export const REPO_URL = 'https://git.spboucher.ai/hfmarketdata.git'
@@ -105,7 +105,7 @@ export default function Layout({ children }) {
105 105 </div>
106 106 <div>
107 107 <h4>Product</h4>
108 − <ul><li><Link to="/docs">Documentation</Link></li><li><Link to="/docs/reference">API reference</Link></li><li><Link to="/playground">Playground</Link></li><li><Link to="/integrations">Integrations</Link></li><li><Link to="/pricing">Pricing / Limits</Link></li></ul>
108 + <ul><li><Link to="/docs">Documentation</Link></li><li><Link to="/docs/reference">API reference</Link></li><li><Link to="/playground">Playground</Link></li><li><Link to="/integrations">Integrations</Link></li><li><Link to="/limits">Access &amp; limits</Link></li></ul>
109 109 </div>
110 110 <div>
111 111 <h4>Resources</h4>
modified hfmarketdata/web/src/app/api.js +2 −0
@@ -3,6 +3,8 @@
3 3 export const BASE_URL = import.meta.env.VITE_API_BASE || ''
4 4 export const PUBLIC_BASE = 'https://www.hfmarketdata.io'
5 5 export const CONTACT_EMAIL = 'contact@spboucher.ai'
6 +// "Request higher limits" CTA — everything is free; the high-usage level is granted on request by e-mail.
7 +export const HIGH_USAGE_MAILTO = `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent('Request for higher limits — HF Market Data')}&body=${encodeURIComponent('Hello,\n\nI would like higher limits on my HF Market Data account.\n\nAccount e-mail: \nWhat I am building: \nExpected volume (requests / rows per minute): \n\nThanks!')}`
6 8
7 9 export const TIERS = [
8 10 { id: 'keyless', name: 'Keyless (per IP)', window: 'hour', requests: 30, rows: 100_000, maxRows: 5_000 },
modified hfmarketdata/web/src/docs/guides.js +1 −1
@@ -43,7 +43,7 @@ export const STATIC_PAGES = [
43 43 ]
44 44
45 45 export const EXTERNAL_LINKS = [
46 − { href: '/pricing', title: 'Limits & pricing', section: 'more' },
46 + { href: '/limits', title: 'Access & limits', section: 'more' },
47 47 { href: '/integrations', title: 'Integrations (MCP, skills)', section: 'more' },
48 48 { href: '/status', title: 'Status', section: 'more' },
49 49 ]
added hfmarketdata/web/src/pages/limits/Limits.jsx +121 −0
@@ -0,0 +1,121 @@
1 +// /limits — Access & limits. The platform is free for everyone; three access levels differ only by limits.
2 +import React from 'react'
3 +import { Link } from 'react-router-dom'
4 +import { CONTACT_EMAIL, HIGH_USAGE_MAILTO, TIERS } from '../../app/api.js'
5 +import Callout from '../../components/Callout.jsx'
6 +import Code from '../../components/Code.jsx'
7 +import { BoltIcon, CheckIcon, KeyIcon, MailIcon } from '../../components/Icons.jsx'
8 +import useTitle from '../../docs/useTitle.js'
9 +import './limits.css'
10 +
11 +const fmt = n => n.toLocaleString('en-US')
12 +const windowLabel = w => (w === 'hour' ? 'per hour' : 'per minute')
13 +
14 +const FAQ = [
15 + { q: 'Is it really all free?', a: 'Yes. Every dataset, endpoint and access level is free. The three levels differ only in how much you can pull per window — and the highest one is granted on request, by e-mail.' },
16 + { q: 'How are requests counted?', a: 'Each HTTP request counts once against the requests window, whatever it returns. Two expensive endpoints (fundamentals screener and frames) count twice. A 304 Not Modified response counts zero.' },
17 + { q: 'How are rows counted?', a: 'The number of data rows in the response (the X-Row-Count header) is deducted from the rows window after the response is produced. Parquet responses count half. Bulk downloads (/v1/bulk/*) count zero. Metadata endpoints return no rows.' },
18 + { q: 'What is a "window"?', a: 'A sliding window evaluated at one-second resolution: one hour for keyless access, one minute for keyed access. If you use your budget in the first seconds, you wait until enough of it has slid out — X-RateLimit-Reset tells you when.' },
19 + { q: 'Which headers tell me where I stand?', a: 'X-RateLimit-Limit-Requests, X-RateLimit-Remaining-Requests, X-RateLimit-Limit-Rows, X-RateLimit-Remaining-Rows, X-RateLimit-Reset and X-Row-Count are on every response. GET /v1/limits returns the same information as JSON.' },
20 + { q: 'What happens on 429?', a: 'The request is not executed. The body is the standard error envelope with code RATE_LIMIT_EXCEEDED and a type telling you which budget ran out (requests_per_hour, rows_per_minute…). Wait Retry-After seconds and retry. Other 4xx errors are not transient — fix the request instead of retrying.' },
21 + { q: 'What is "max rows per request"?', a: 'The largest limit= value your level accepts on a single call. Asking for more returns 400 ROW_LIMIT_EXCEEDED. Use CSV (up to 2 000 000 rows on v1 bars), Parquet, pagination with cursor, or bulk downloads for larger extracts.' },
22 + { q: 'I am behind a corporate network and hit the keyless limit immediately.', a: 'Keyless access is metered per IP, and a shared egress means a shared budget. Create a free account: keys are metered individually.' },
23 + { q: 'How fast is the high-usage request handled?', a: 'Send the e-mail from the address of your account with a sentence on what you are building and the expected volume. The higher limits are applied to your existing keys; nothing changes in your code.' },
24 +]
25 +
26 +export default function Limits() {
27 + useTitle('Access & limits — free for everyone', 'HF Market Data is free. Keyless access is very limited, a free account gives much higher limits, and higher limits are granted on request by e-mail.')
28 + return (
29 + <main className="page limits" data-testid="limits-page">
30 + <header className="limits-head">
31 + <p className="eyebrow">Access &amp; limits</p>
32 + <h1>Free for everyone.</h1>
33 + <p className="lead">There is nothing to buy. Keyless access is deliberately small so you can try things without signing up; a free account raises the limits a lot; and if you need more, you simply ask.</p>
34 + <div className="limits-cta">
35 + <Link to="/signup" className="btn btn-primary btn-lg" data-testid="cta-signup"><KeyIcon /> Create free account</Link>
36 + <a href={HIGH_USAGE_MAILTO} className="btn btn-lg" data-testid="cta-high-usage"><MailIcon /> Request higher limits → {CONTACT_EMAIL}</a>
37 + </div>
38 + </header>
39 +
40 + <section aria-labelledby="tiers-heading">
41 + <h2 id="tiers-heading" className="sr-only">Access levels</h2>
42 + <div className="tiers" data-testid="tiers">
43 + {TIERS.map((t, i) => (
44 + <article key={t.id} className={`tier ${i === 1 ? 'tier-featured' : ''}`} data-testid={`tier-${t.id}`}>
45 + <header>
46 + <h3>{['Keyless', 'Free account', 'High usage'][i]}</h3>
47 + <p className="muted">{['Per IP · nothing to do', 'API key · sign up with an e-mail', 'API key · on request, free'][i]}</p>
48 + </header>
49 + <dl className="tier-numbers">
50 + <div><dt>Requests</dt><dd><strong>{fmt(t.requests)}</strong> <span>{windowLabel(t.window)}</span></dd></div>
51 + <div><dt>Rows</dt><dd><strong>{fmt(t.rows)}</strong> <span>{windowLabel(t.window)}</span></dd></div>
52 + <div><dt>Max rows / request</dt><dd><strong>{fmt(t.maxRows)}</strong></dd></div>
53 + <div><dt>Window</dt><dd><strong>1 {t.window}</strong> <span>sliding</span></dd></div>
54 + <div><dt>Datasets</dt><dd><strong>All</strong> <span>same data at every level</span></dd></div>
55 + <div><dt>Cost</dt><dd><strong>Free</strong></dd></div>
56 + </dl>
57 + <footer>
58 + {i === 0 && <Link to="/playground" className="btn">Try it keyless</Link>}
59 + {i === 1 && <Link to="/signup" className="btn btn-primary">Create free account</Link>}
60 + {i === 2 && <a href={HIGH_USAGE_MAILTO} className="btn">Request by e-mail</a>}
61 + </footer>
62 + </article>
63 + ))}
64 + </div>
65 + <p className="muted small">The same numbers are published by the API in <code>info.x-tiers</code> of <a href="/openapi.json">openapi.json</a> and returned for your own key by <code>GET /v1/limits</code>.</p>
66 + </section>
67 +
68 + <section className="limits-incentives">
69 + <Callout type="tip" title="Three ways to stretch any level">
70 + <ul>
71 + <li><strong>Parquet responses count half</strong> — <code>format=parquet</code> on v2 endpoints is charged 50 % of the rows returned.</li>
72 + <li><strong>Bulk downloads are outside the rows quota</strong> — <code>/v1/bulk/*</code> whole-universe Parquet extracts count as one request and zero rows.</li>
73 + <li><strong>304 Not Modified is free</strong> — send <code>If-None-Match</code> with the ETag you received; unchanged data costs nothing.</li>
74 + </ul>
75 + </Callout>
76 + </section>
77 +
78 + <section className="limits-headers">
79 + <h2>Every response tells you where you stand</h2>
80 + <div className="grid grid-2">
81 + <Code language="http" title="Response headers" copy={false} code={`HTTP/1.1 200 OK
82 +X-RateLimit-Limit-Requests: 120
83 +X-RateLimit-Remaining-Requests: 118
84 +X-RateLimit-Limit-Rows: 1000000
85 +X-RateLimit-Remaining-Rows: 994800
86 +X-RateLimit-Reset: 1757030460
87 +X-Row-Count: 5000`} />
88 + <Code language="json" title="429 Too Many Requests" copy={false} code={JSON.stringify({ error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Request quota exhausted: 30 requests per hour for keyless access. Create a free account for 120 requests per minute.', docs: 'https://www.hfmarketdata.io/docs/errors#rate_limit_exceeded', type: 'requests_per_hour' }, detail: 'Request quota exhausted: 30 requests per hour for keyless access.' }, null, 2)} />
89 + </div>
90 + <p className="muted">Handling code in curl, Python, JavaScript and R is in the <Link to="/docs/rate-limits">rate limits guide</Link>; every error code is on the <Link to="/docs/errors">errors page</Link>.</p>
91 + </section>
92 +
93 + <section className="limits-faq" aria-labelledby="faq-heading">
94 + <h2 id="faq-heading">Questions</h2>
95 + <div className="faq">
96 + {FAQ.map(f => (
97 + <details key={f.q}>
98 + <summary>{f.q}</summary>
99 + <p>{f.a}</p>
100 + </details>
101 + ))}
102 + </div>
103 + </section>
104 +
105 + <section className="limits-final">
106 + <div className="card limits-final-card">
107 + <BoltIcon />
108 + <div>
109 + <h3>Need more than the free account gives you?</h3>
110 + <p className="muted">Write to <a href={HIGH_USAGE_MAILTO}>{CONTACT_EMAIL}</a> with what you are building. Higher limits are switched on your existing keys — free, on request.</p>
111 + </div>
112 + <ul className="checks">
113 + <li><CheckIcon /> 600 requests / minute</li>
114 + <li><CheckIcon /> 10 000 000 rows / minute</li>
115 + <li><CheckIcon /> 200 000 rows / request</li>
116 + </ul>
117 + </div>
118 + </section>
119 + </main>
120 + )
121 +}
added hfmarketdata/web/src/pages/limits/limits.css +39 −0
@@ -0,0 +1,39 @@
1 +.limits-head { max-width: 760px; margin-bottom: 36px; }
2 +.limits-head h1 { font-size: clamp(34px, 5vw, 52px); }
3 +.limits-cta { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 22px; }
4 +.tiers { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-bottom: 14px; }
5 +.tier { display: flex; flex-direction: column; gap: 18px; padding: 24px; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--bg-1); }
6 +.tier-featured { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent), var(--shadow); position: relative; }
7 +.tier-featured::before { content: "Most people need this"; position: absolute; top: -11px; left: 20px; font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; background: var(--accent); color: var(--accent-ink); padding: 3px 9px; border-radius: 999px; }
8 +.tier header h3 { font-size: 20px; margin-bottom: 2px; }
9 +.tier header p { margin: 0; font-size: 13px; }
10 +.tier-numbers { display: grid; gap: 10px; margin: 0; }
11 +.tier-numbers div { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
12 +.tier-numbers dt { font-size: 13px; color: var(--fg-2); }
13 +.tier-numbers dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
14 +.tier-numbers dd strong { font-size: 16px; font-weight: 650; }
15 +.tier-numbers dd span { display: block; font-size: 12px; color: var(--fg-3); }
16 +.tier footer { margin-top: auto; }
17 +.tier footer .btn { width: 100%; }
18 +.limits-incentives { margin: 36px 0; }
19 +.limits-incentives ul { margin: 0; padding-left: 1.2em; }
20 +.limits-incentives li + li { margin-top: 6px; }
21 +.limits-headers { margin: 40px 0; }
22 +.limits-faq { margin: 48px 0; max-width: 820px; }
23 +.faq details { border-bottom: 1px solid var(--line); }
24 +.faq summary { padding: 14px 4px; cursor: pointer; font-weight: 550; list-style: none; display: flex; justify-content: space-between; gap: 12px; }
25 +.faq summary::-webkit-details-marker { display: none; }
26 +.faq summary::after { content: "+"; color: var(--fg-3); font-weight: 400; }
27 +.faq details[open] summary::after { content: "–"; }
28 +.faq p { margin: 0 4px 16px; color: var(--fg-1); max-width: 70ch; }
29 +.limits-final-card { display: grid; grid-template-columns: auto 1fr auto; gap: 20px; align-items: center; padding: 26px; }
30 +.limits-final-card > svg { width: 34px; height: 34px; color: var(--accent); }
31 +.limits-final-card h3 { margin-bottom: 4px; }
32 +.limits-final-card p { margin: 0; }
33 +.checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; font-size: 14px; }
34 +.checks li { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
35 +.checks svg { width: 16px; height: 16px; color: var(--accent); }
36 +@media (max-width: 900px) {
37 + .tiers { grid-template-columns: 1fr; }
38 + .limits-final-card { grid-template-columns: 1fr; }
39 +}
modified hfmarketdata/web/src/pages/pricing/Pricing.jsx +3 −5
@@ -1,5 +1,3 @@
1 −import React from 'react'
2 −// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 −export default function Pricing() {
4 − return <main className="page"><h1>Pricing</h1><p className="muted">Coming soon.</p></main>
5 −}
1 +// TEMPORARY shim — keeps `npm run build` green until src/App.jsx routes `/limits` (and redirects `/pricing`).
2 +// The real page is src/pages/limits/Limits.jsx. Delete this folder when App.jsx is updated.
3 +export { default } from '../limits/Limits.jsx'
6 4