| 1 |
|
−import React from 'react' |
| 2 |
|
−// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. |
|
1 |
+// Homepage: hero, get-a-key steps, live playground preview, integrations, live dataset counters, fundamentals chart. |
|
2 |
+// Every live call degrades gracefully to a static sample; no external requests besides the API itself. |
|
3 |
+import React, { useEffect, useMemo, useRef, useState } from 'react' |
|
4 |
+import { Link } from 'react-router-dom' |
|
5 |
+import { BASE_URL, CONTACT_EMAIL, HIGH_USAGE_MAILTO, PUBLIC_BASE, api } from '../../app/api.js' |
|
6 |
+import Code, { CopyButton } from '../../components/Code.jsx' |
|
7 |
+import CodeTabs from '../../components/CodeTabs.jsx' |
|
8 |
+import { ArrowRightIcon, BoltIcon, DatabaseIcon, KeyIcon, MailIcon, PlayIcon, PulseIcon } from '../../components/Icons.jsx' |
|
9 |
+import { snippetsFor } from '../../docs/snippets.js' |
|
10 |
+import useTitle from '../../docs/useTitle.js' |
|
11 |
+import './home.css' |
|
12 |
+ |
|
13 |
+const PREVIEW_PATH = '/v1/bars/futures/ES?timeframe=1day&limit=5' |
|
14 |
+// Real response captured from production on 2026-09-04 (first five daily ES continuous bars, contin_UNadj). |
|
15 |
+const PREVIEW_FALLBACK = { count: 5, data: [ |
|
16 |
+ { ticker: 'ES', datetime: '2008-01-02', open: 1478.75, high: 1482.75, low: 1449.0, close: 1458.5, volume: 1623508.0, open_interest: 1764399.0 }, |
|
17 |
+ { ticker: 'ES', datetime: '2008-01-03', open: 1458.5, high: 1464.75, low: 1451.0, close: 1458.75, volume: 1266237.0, open_interest: 1758867.0 }, |
|
18 |
+ { ticker: 'ES', datetime: '2008-01-04', open: 1458.75, high: 1463.25, low: 1417.5, close: 1423.0, volume: 2199479.0, open_interest: 1882526.0 }, |
|
19 |
+ { ticker: 'ES', datetime: '2008-01-07', open: 1422.5, high: 1432.25, low: 1410.0, close: 1421.5, volume: 2436934.0, open_interest: 1884889.0 }, |
|
20 |
+ { ticker: 'ES', datetime: '2008-01-08', open: 1421.75, high: 1437.75, low: 1393.0, close: 1397.0, volume: 2902242.0, open_interest: 1979612.0 }, |
|
21 |
+] } |
|
22 |
+ |
|
23 |
+const fmt = n => (n == null ? '—' : Number(n).toLocaleString('en-US')) |
|
24 |
+ |
| 3 |
25 |
export default function Home() { |
| 4 |
|
− return <main className="page"><h1>Home</h1><p className="muted">Coming soon.</p></main> |
|
26 |
+ useTitle(null, 'Free open API for high-frequency historical market data: stocks, ETFs, futures contracts, crypto, indices, FX from 1-minute to daily, options chains with Greeks and SEC EDGAR fundamentals since 2010.') |
|
27 |
+ return ( |
|
28 |
+ <main className="home" data-testid="home"> |
|
29 |
+ <Hero /> |
|
30 |
+ <Datasets /> |
|
31 |
+ <GetKey /> |
|
32 |
+ <Preview /> |
|
33 |
+ <Fundamentals /> |
|
34 |
+ <Integrations /> |
|
35 |
+ <Closing /> |
|
36 |
+ </main> |
|
37 |
+ ) |
|
38 |
+} |
|
39 |
+ |
|
40 |
+function Hero() { |
|
41 |
+ const curl = `curl "${PUBLIC_BASE}/v1/bars/stock/AAPL?timeframe=1min&limit=5"` |
|
42 |
+ return ( |
|
43 |
+ <section className="hero"> |
|
44 |
+ <div className="hero-inner"> |
|
45 |
+ <p className="eyebrow">Free · open · no signup to start</p> |
|
46 |
+ <h1>Open high-frequency market data.<br /><span className="grad">1-minute to daily, since 2010.</span></h1> |
|
47 |
+ <p className="lead">Stocks, ETFs, futures — continuous series <em>and</em> every individual contract — crypto, indices and FX, plus full options chains with Greeks and point-in-time SEC fundamentals. One base URL, JSON / CSV / Parquet, generated docs, a playground, and limits that only grow.</p> |
|
48 |
+ <div className="hero-cta"> |
|
49 |
+ <Link to="/docs/quickstart" className="btn btn-primary btn-lg">Read the quickstart <ArrowRightIcon /></Link> |
|
50 |
+ <Link to="/playground" className="btn btn-lg"><PlayIcon /> Open the playground</Link> |
|
51 |
+ </div> |
|
52 |
+ <div className="hero-curl" role="group" aria-label="First request"> |
|
53 |
+ <code>{curl}</code> |
|
54 |
+ <CopyButton text={curl} /> |
|
55 |
+ </div> |
|
56 |
+ </div> |
|
57 |
+ <HeroArt /> |
|
58 |
+ </section> |
|
59 |
+ ) |
|
60 |
+} |
|
61 |
+ |
|
62 |
+function HeroArt() { |
|
63 |
+ // Decorative candlesticks drawn from the fallback ES bars (real data), scaled — no external asset. |
|
64 |
+ const bars = PREVIEW_FALLBACK.data |
|
65 |
+ const lo = Math.min(...bars.map(b => b.low)), hi = Math.max(...bars.map(b => b.high)) |
|
66 |
+ const y = v => 20 + (1 - (v - lo) / (hi - lo)) * 160 |
|
67 |
+ return ( |
|
68 |
+ <svg className="hero-art" viewBox="0 0 320 200" aria-hidden="true"> |
|
69 |
+ <defs><linearGradient id="hg" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stopColor="var(--accent)" stopOpacity=".35" /><stop offset="1" stopColor="var(--accent)" stopOpacity="0" /></linearGradient></defs> |
|
70 |
+ {[0, 1, 2, 3, 4].map(i => <line key={i} x1="0" x2="320" y1={20 + i * 40} y2={20 + i * 40} stroke="var(--line)" />)} |
|
71 |
+ {bars.map((b, i) => { |
|
72 |
+ const x = 40 + i * 60 |
|
73 |
+ const up = b.close >= b.open |
|
74 |
+ return ( |
|
75 |
+ <g key={b.datetime}> |
|
76 |
+ <line x1={x} x2={x} y1={y(b.high)} y2={y(b.low)} stroke={up ? 'var(--accent)' : 'var(--danger)'} strokeWidth="2" /> |
|
77 |
+ <rect x={x - 12} width="24" y={y(Math.max(b.open, b.close))} height={Math.max(2, Math.abs(y(b.open) - y(b.close)))} fill={up ? 'var(--accent)' : 'var(--danger)'} rx="2" /> |
|
78 |
+ </g> |
|
79 |
+ ) |
|
80 |
+ })} |
|
81 |
+ <path d={`M40 ${y(bars[0].close)} ${bars.map((b, i) => `L${40 + i * 60} ${y(b.close)}`).join(' ')} L280 200 L40 200 Z`} fill="url(#hg)" /> |
|
82 |
+ </svg> |
|
83 |
+ ) |
|
84 |
+} |
|
85 |
+ |
|
86 |
+function useStatus() { |
|
87 |
+ const [status, setStatus] = useState(null) |
|
88 |
+ useEffect(() => { |
|
89 |
+ let alive = true |
|
90 |
+ api('/v1/status').then(({ data }) => alive && setStatus(data), () => {}) |
|
91 |
+ return () => { alive = false } |
|
92 |
+ }, []) |
|
93 |
+ return status |
|
94 |
+} |
|
95 |
+ |
|
96 |
+export function countInstruments(datasets, asset) { |
|
97 |
+ const d = datasets?.[asset] |
|
98 |
+ if (!d) return null |
|
99 |
+ const tf = d['1day'] || d['1min'] || Object.values(d)[0] |
|
100 |
+ if (!tf || typeof tf !== 'object') return null |
|
101 |
+ return Math.max(...Object.values(tf).map(Number).filter(Number.isFinite)) |
|
102 |
+} |
|
103 |
+ |
|
104 |
+function Datasets() { |
|
105 |
+ const status = useStatus() |
|
106 |
+ const ds = status?.datasets |
|
107 |
+ const cards = [ |
|
108 |
+ { k: 'stock', label: 'Stocks', sub: 'US equities · 5 timeframes · 3 adjustments' }, |
|
109 |
+ { k: 'etf', label: 'ETFs', sub: 'same variants as stocks' }, |
|
110 |
+ { k: 'futures', label: 'Futures roots', sub: 'continuous · unadjusted, ratio, absolute' }, |
|
111 |
+ { k: 'futures_contracts', label: 'Futures contracts', sub: 'individual expiries, daily to 1-minute' }, |
|
112 |
+ { k: 'crypto', label: 'Crypto pairs', sub: '24 × 7 bars' }, |
|
113 |
+ { k: 'index', label: 'Indices', sub: 'equity & volatility indices' }, |
|
114 |
+ { k: 'fx', label: 'FX pairs', sub: 'majors, crosses, EM' }, |
|
115 |
+ ] |
|
116 |
+ const quarters = ds?.options?.quarters?.length |
|
117 |
+ return ( |
|
118 |
+ <section className="section datasets" aria-labelledby="datasets-h"> |
|
119 |
+ <div className="section-head"> |
|
120 |
+ <p className="eyebrow"><DatabaseIcon /> Datasets</p> |
|
121 |
+ <h2 id="datasets-h">What is in the lake — live from <code>/v1/status</code></h2> |
|
122 |
+ <p className="muted">Counts refresh from the API each time this page loads. {status ? '' : 'Loading live counts…'}</p> |
|
123 |
+ </div> |
|
124 |
+ <div className="counters" data-testid="counters"> |
|
125 |
+ {cards.map(c => ( |
|
126 |
+ <div key={c.k} className="counter"> |
|
127 |
+ <div className="counter-n">{ds ? fmt(countInstruments(ds, c.k)) : <span className="skeleton" />}</div> |
|
128 |
+ <div className="counter-l">{c.label}</div> |
|
129 |
+ <div className="counter-s">{c.sub}</div> |
|
130 |
+ </div> |
|
131 |
+ ))} |
|
132 |
+ <div className="counter"> |
|
133 |
+ <div className="counter-n">{ds ? fmt(quarters) : <span className="skeleton" />}</div> |
|
134 |
+ <div className="counter-l">Options quarters</div> |
|
135 |
+ <div className="counter-s">end-of-day chains with Greeks{ds?.options?.files_latest_quarter ? ` · ${fmt(ds.options.files_latest_quarter)} underlyings last quarter` : ''}</div> |
|
136 |
+ </div> |
|
137 |
+ </div> |
|
138 |
+ <p className="muted small">Full inventory by timeframe and adjustment on the <Link to="/status">status page</Link>.</p> |
|
139 |
+ </section> |
|
140 |
+ ) |
|
141 |
+} |
|
142 |
+ |
|
143 |
+function GetKey() { |
|
144 |
+ return ( |
|
145 |
+ <section className="section getkey" aria-labelledby="getkey-h"> |
|
146 |
+ <div className="section-head"> |
|
147 |
+ <p className="eyebrow"><KeyIcon /> Access</p> |
|
148 |
+ <h2 id="getkey-h">Get an API key — or don't</h2> |
|
149 |
+ <p className="muted">Everything is free. The three levels differ only in how much you can pull per window.</p> |
|
150 |
+ </div> |
|
151 |
+ <ol className="steps"> |
|
152 |
+ <li className="step"> |
|
153 |
+ <span className="step-n">1</span> |
|
154 |
+ <h3>Start keyless</h3> |
|
155 |
+ <p>Paste a URL. Per IP: <strong>30 requests / hour</strong>, 100 000 rows / hour, 5 000 rows per request. Enough to explore every endpoint.</p> |
|
156 |
+ <Link to="/playground" className="btn btn-sm">Try in the playground</Link> |
|
157 |
+ </li> |
|
158 |
+ <li className="step step-featured"> |
|
159 |
+ <span className="step-n">2</span> |
|
160 |
+ <h3>Create a free account</h3> |
|
161 |
+ <p>An e-mail address gets you a key with <strong>120 requests and 1 000 000 rows / minute</strong>, 50 000 rows per request. Keys are shown once, rotatable, revocable.</p> |
|
162 |
+ <Link to="/signup" className="btn btn-sm btn-primary">Create free account</Link> |
|
163 |
+ </li> |
|
164 |
+ <li className="step"> |
|
165 |
+ <span className="step-n">3</span> |
|
166 |
+ <h3>Ask for higher limits</h3> |
|
167 |
+ <p>Building something bigger? E-mail <a href={HIGH_USAGE_MAILTO}>{CONTACT_EMAIL}</a> and your key moves to <strong>600 requests and 10 000 000 rows / minute</strong>. Also free — on request.</p> |
|
168 |
+ <a href={HIGH_USAGE_MAILTO} className="btn btn-sm"><MailIcon /> Request higher limits</a> |
|
169 |
+ </li> |
|
170 |
+ </ol> |
|
171 |
+ <p className="muted small">Parquet counts half, bulk downloads count zero, 304 responses are free. <Link to="/limits">All limits & how they are counted →</Link></p> |
|
172 |
+ </section> |
|
173 |
+ ) |
|
174 |
+} |
|
175 |
+ |
|
176 |
+function Preview() { |
|
177 |
+ const [state, setState] = useState({ body: PREVIEW_FALLBACK, live: false, ms: null, rate: null }) |
|
178 |
+ useEffect(() => { |
|
179 |
+ let alive = true |
|
180 |
+ api(PREVIEW_PATH).then(({ data, ms, rate }) => alive && setState({ body: data, live: true, ms, rate }), () => {}) |
|
181 |
+ return () => { alive = false } |
|
182 |
+ }, []) |
|
183 |
+ const snippets = useMemo(() => snippetsFor({ url: PUBLIC_BASE + PREVIEW_PATH }), []) |
|
184 |
+ return ( |
|
185 |
+ <section className="section preview" aria-labelledby="preview-h"> |
|
186 |
+ <div className="section-head"> |
|
187 |
+ <p className="eyebrow"><PlayIcon /> Playground</p> |
|
188 |
+ <h2 id="preview-h">A real request, a real response</h2> |
|
189 |
+ <p className="muted">Five daily bars of the E-mini S&P 500 continuous series — {state.live ? <>fetched live in <strong>{state.ms} ms</strong>{state.rate?.remainingRequests != null ? <>, {fmt(state.rate.remainingRequests)} keyless requests left this hour</> : ''}</> : 'captured from production (the live call did not complete)'}. Change anything in the <Link to="/playground">playground</Link>.</p> |
|
190 |
+ </div> |
|
191 |
+ <div className="preview-grid"> |
|
192 |
+ <CodeTabs title="Request" snippets={snippets} maxHeight={360} /> |
|
193 |
+ <Code title={`Response · 200${state.live ? ' · live' : ' · captured'}`} language="json" maxHeight={360} code={JSON.stringify(state.body, null, 2)} /> |
|
194 |
+ </div> |
|
195 |
+ <div className="preview-actions"> |
|
196 |
+ <Link to="/playground?ep=bars_v1_bars__asset___ticker__get&asset=futures&ticker=ES&timeframe=1day&limit=5" className="btn btn-primary">Open this request in the playground <ArrowRightIcon /></Link> |
|
197 |
+ <Link to="/docs/reference" className="btn">Browse all endpoints</Link> |
|
198 |
+ </div> |
|
199 |
+ </section> |
|
200 |
+ ) |
|
201 |
+} |
|
202 |
+ |
|
203 |
+function Fundamentals() { |
|
204 |
+ const ref = useRef(null) |
|
205 |
+ const [meta, setMeta] = useState({ source: null, points: 0, first: null, last: null, latest: null }) |
|
206 |
+ useEffect(() => { |
|
207 |
+ let alive = true, chart = null, onResize = null |
|
208 |
+ ;(async () => { |
|
209 |
+ let points = null, source = null |
|
210 |
+ try { |
|
211 |
+ const { data } = await api('/v1/fundamentals/AAPL/ratios/daily?from=2015-01-01&limit=50000') |
|
212 |
+ const rows = data?.data || [] |
|
213 |
+ const pick = r => r.pe ?? r.pe_ratio ?? r.price_to_earnings |
|
214 |
+ const pts = rows.filter(r => pick(r) != null).map(r => ({ time: (r.date || r.datetime || '').slice(0, 10), value: Number(pick(r)) })) |
|
215 |
+ if (pts.length > 50) { points = pts; source = 'live' } |
|
216 |
+ } catch { /* not live yet */ } |
|
217 |
+ if (!points) { |
|
218 |
+ const mod = await import('../../../content/samples/aapl-pe.json') |
|
219 |
+ const sample = mod.default || mod |
|
220 |
+ points = sample.points.map(p => ({ time: p.date, value: p.pe })) |
|
221 |
+ source = 'sample' |
|
222 |
+ } |
|
223 |
+ if (!alive || !ref.current) return |
|
224 |
+ const { createChart, LineStyle } = await import('lightweight-charts') |
|
225 |
+ if (!alive || !ref.current) return |
|
226 |
+ const dark = document.documentElement.getAttribute('data-theme') !== 'light' |
|
227 |
+ const colors = () => ({ text: dark ? '#aeb6c4' : '#66707f', grid: dark ? '#232833' : '#e2e5eb' }) |
|
228 |
+ chart = createChart(ref.current, { |
|
229 |
+ height: 260, layout: { background: { color: 'transparent' }, textColor: colors().text, fontFamily: 'inherit', attributionLogo: false }, |
|
230 |
+ grid: { vertLines: { color: colors().grid, style: LineStyle.Dotted }, horzLines: { color: colors().grid, style: LineStyle.Dotted } }, |
|
231 |
+ rightPriceScale: { borderVisible: false }, timeScale: { borderVisible: false, fixLeftEdge: true, fixRightEdge: true }, |
|
232 |
+ handleScroll: false, handleScale: false, crosshair: { mode: 1 }, |
|
233 |
+ }) |
|
234 |
+ const series = chart.addLineSeries({ color: dark ? '#5ee7a5' : '#0d8a55', lineWidth: 2, priceLineVisible: false, lastValueVisible: true, priceFormat: { type: 'price', precision: 1, minMove: 0.1 } }) |
|
235 |
+ series.setData(points) |
|
236 |
+ chart.timeScale().fitContent() |
|
237 |
+ onResize = () => chart.applyOptions({ width: ref.current.clientWidth }) |
|
238 |
+ onResize(); window.addEventListener('resize', onResize) |
|
239 |
+ setMeta({ source, points: points.length, first: points[0].time.slice(0, 4), last: points.at(-1).time.slice(0, 4), latest: points.at(-1).value }) |
|
240 |
+ })() |
|
241 |
+ return () => { alive = false; if (onResize) window.removeEventListener('resize', onResize); chart?.remove() } |
|
242 |
+ }, []) |
|
243 |
+ return ( |
|
244 |
+ <section className="section fundamentals" aria-labelledby="fund-h"> |
|
245 |
+ <div className="fund-grid"> |
|
246 |
+ <div> |
|
247 |
+ <p className="eyebrow"><PulseIcon /> Fundamentals & ratios</p> |
|
248 |
+ <h2 id="fund-h">SEC EDGAR, point-in-time, since 2010</h2> |
|
249 |
+ <p className="muted">Balance sheets, income and cash-flow statements exactly as filed, with the filing date on every number — so a backtest only sees what was public. Dozens of ratios per period, market multiples per trading day, a screener across the universe, bulk Parquet per year and a WebSocket stream of new filings.</p> |
|
250 |
+ <ul className="fund-list"> |
|
251 |
+ <li><Link to="/docs/fundamentals/balance-sheet">Reading a balance sheet through the API</Link> — Apple Q2 FY2024, line by line</li> |
|
252 |
+ <li><Link to="/docs/fundamentals/point-in-time">Point-in-time & look-ahead bias</Link></li> |
|
253 |
+ <li><Link to="/docs/fundamentals/ratios">Every ratio formula</Link> · <Link to="/docs/fundamentals/screener">Screener</Link> · <Link to="/docs/fundamentals/stream">Filings stream</Link></li> |
|
254 |
+ </ul> |
|
255 |
+ </div> |
|
256 |
+ <figure className="fund-chart card"> |
|
257 |
+ <figcaption> |
|
258 |
+ <strong>Apple — P/E, {meta.first || '2015'}–{meta.last || 'today'}</strong> |
|
259 |
+ <span className="muted small">{meta.source === 'live' ? <>live from <code>/v1/fundamentals/AAPL/ratios/daily</code></> : meta.source === 'sample' ? <>sample: split-adjusted close ÷ 10-K diluted EPS (live ratios endpoint not reachable)</> : 'loading…'}{meta.latest ? ` · latest ${meta.latest.toFixed(1)}×` : ''}</span> |
|
260 |
+ </figcaption> |
|
261 |
+ <div ref={ref} className="chart" data-testid="pe-chart" /> |
|
262 |
+ </figure> |
|
263 |
+ </div> |
|
264 |
+ </section> |
|
265 |
+ ) |
|
266 |
+} |
|
267 |
+ |
|
268 |
+function Integrations() { |
|
269 |
+ const items = [ |
|
270 |
+ { name: 'Claude Code', desc: 'Add the MCP server and ask for data in plain English.', logo: <svg viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="#d97757" /><path d="M12 28 20 12l8 16h-4l-4-8-4 8z" fill="#fff" /></svg> }, |
|
271 |
+ { name: 'Cursor', desc: 'One-line MCP config in .cursor/mcp.json.', logo: <svg viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="#111" /><path d="M20 8l11 6.5v11L20 32 9 25.5v-11z" fill="none" stroke="#fff" strokeWidth="2" /><path d="M20 8v24M9 14.5l11 6.5 11-6.5" stroke="#fff" strokeWidth="1.5" fill="none" /></svg> }, |
|
272 |
+ { name: 'Codex', desc: 'Skills pack with ready-made recipes for the CLI.', logo: <svg viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="#0a0c10" stroke="#313847" /><circle cx="20" cy="20" r="9" fill="none" stroke="#fff" strokeWidth="2.2" /><circle cx="20" cy="20" r="3" fill="#fff" /></svg> }, |
|
273 |
+ { name: 'MCP', desc: 'Model Context Protocol server, stdio, any compatible client.', logo: <svg viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="var(--bg-2)" stroke="var(--line-2)" /><path d="M12 26l8-14 8 14M15 22h10" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" /></svg> }, |
|
274 |
+ ] |
|
275 |
+ return ( |
|
276 |
+ <section className="section integrations" aria-labelledby="int-h"> |
|
277 |
+ <div className="section-head"> |
|
278 |
+ <p className="eyebrow"><BoltIcon /> Integrations</p> |
|
279 |
+ <h2 id="int-h">Works where you already work</h2> |
|
280 |
+ <p className="muted">An MCP server exposes every endpoint as a tool; a skills pack teaches coding agents the recipes. Install once, ask questions.</p> |
|
281 |
+ </div> |
|
282 |
+ <div className="int-grid"> |
|
283 |
+ {items.map(it => ( |
|
284 |
+ <Link key={it.name} to="/integrations" className="int-card"> |
|
285 |
+ <span className="int-logo">{it.logo}</span> |
|
286 |
+ <span><strong>{it.name}</strong><span className="muted">{it.desc}</span></span> |
|
287 |
+ </Link> |
|
288 |
+ ))} |
|
289 |
+ </div> |
|
290 |
+ </section> |
|
291 |
+ ) |
|
292 |
+} |
|
293 |
+ |
|
294 |
+function Closing() { |
|
295 |
+ return ( |
|
296 |
+ <section className="section closing"> |
|
297 |
+ <div className="closing-card"> |
|
298 |
+ <div> |
|
299 |
+ <h2>Start with a URL. Grow into a key.</h2> |
|
300 |
+ <p className="muted">No signup for your first request, a free account when you need throughput, higher limits on request. Documentation generated from the live spec, every example runnable.</p> |
|
301 |
+ </div> |
|
302 |
+ <div className="hero-cta"> |
|
303 |
+ <Link to="/docs" className="btn btn-primary btn-lg">Documentation</Link> |
|
304 |
+ <Link to="/signup" className="btn btn-lg">Create free account</Link> |
|
305 |
+ </div> |
|
306 |
+ </div> |
|
307 |
+ <p className="muted small" style={{ textAlign: 'center' }}>API base <code>{BASE_URL || PUBLIC_BASE}/v1</code> · <a href="/openapi.json">openapi.json</a> · <Link to="/status">status</Link></p> |
|
308 |
+ </section> |
|
309 |
+ ) |
| 5 |
310 |
} |