> Source: https://ashareapi.com/en/docs/guides/chatgpt-sites-dashboard/  ·  Markdown version for LLMs / AI agents

Tutorial

# Building an A-Share Sentiment Dashboard with ChatGPT Sites
 ChatGPT Sites turns a prompt into a web page, and the data comes from one fetch in the browser. The free endpoints send access-control-allow-origin: *, so no backend and no key — call them straight from the page. Runnable code below.

## Why no backend? Because cross-origin calls are already allowed
 Before a browser calls another domain it asks whether that domain allows it. Both free endpoints return `access-control-allow-origin: *` (verified 2026-10-28), so a plain `fetch` on the page gets the data — **no proxy server required**.
 Verify it yourself; the response headers must contain that line:

 Command line (verify the CORS header)
```
curl -s -D - -o /dev/null \
 -H "Origin: https://example.chatgpt.site" \
 https://api.ashareapi.com/v1/changedist | grep -i access-control

# access-control-allow-origin: *
```

## Decide what the dashboard shows
 The first three rows are enough for a sentiment dashboard. See the endpoint list for all 96 endpoints, 7 of which are free and need no signup.
 |
| | Reading | Endpoint | Key required | Unit

| | Advancers / decliners | `/v1/changedist` | No | count

| | Limit-ups / limit-downs | `/v1/changedist` | No | count

| | Two-market turnover | `/v1/changedist` | No | CNY 100M (inside `data` text)

| | Last price of a stock | `/v1/quote?code=sh600519` | No | CNY

| | Hot search ranking | `/v1/hot` | No | —

 Advancers / decliners
 `/v1/changedist`
 No
 count

 Limit-ups / limit-downs
 `/v1/changedist`
 No
 count

 Two-market turnover
 `/v1/changedist`
 No
 CNY 100M (inside `data` text)

 Last price of a stock
 `/v1/quote?code=sh600519`
 No
 CNY

 Hot search ranking
 `/v1/hot`
 No
 —

## Step 1: hand the page description to ChatGPT
 Create a Site and paste the prompt below. It defines the layout; the data wiring comes next.

 Prompt (paste as-is)
```
Build a single-page A-share sentiment dashboard:
1. Four cards on top: advancers, decliners, limit-ups, two-market turnover
2. Fetch data from https://api.ashareapi.com/v1/changedist
 directly in the browser, no backend
3. Red for up, green for down, dark background
4. Auto-refresh every 30 seconds
```
 The generated site lives at ` . .chatgpt.site`. The model has no knowledge of the real field names when it writes the page, so its data layer usually does not match — replace it with the code in the next step.

## Step 2: wire the data in (complete code, replace wholesale)
 The following is written **against the live response**. Swap out the generated ` `, or replace the whole file:

 HTML + vanilla JS (no dependencies, runnable)
```
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>A-share sentiment</title>
<style>
 body{margin:0;padding:24px;background:#0b0b0f;color:#e9e9f2;
 font:14px/1.5 -apple-system,"Segoe UI",sans-serif}
 .row{display:flex;gap:12px;flex-wrap:wrap}
 .card{min-width:120px;padding:14px 18px;border-radius:10px;
 background:#16161d;border:1px solid #2a2a36}
 .k{font-size:12px;color:#8a8aa0}
 .v{margin-top:4px;font-size:26px;font-weight:600}
 .up{color:#ff4d4f}.down{color:#00c853}
 #ts{margin-top:16px;font-size:12px;color:#8a8aa0}
</style>
<div class="row" id="cards"></div>
<div id="ts"></div>

<script>
var API = "https://api.ashareapi.com/v1";

function card(k, v, cls) {
 return '<div class="card"><div class="k">' + k + '</div>' +
 '<div class="v ' + (cls || "") + '">' + v + '</div></div>';
}

async function loadMood() {
 var r = await fetch(API + "/changedist");
 var b = await r.json();
 if (!b.ok) throw new Error("changedist failed");

 var s = (b.structured && b.structured[0]) || {}; // keys are Chinese
 var m = String(b.data).match(/两市成交额：([\d.]+)亿/); // turnover lives in data text

 document.getElementById("cards").innerHTML =
 card("Advancers", s["上涨"], "up") +
 card("Decliners", s["下跌"], "down") +
 card("Limit-ups", s["涨停"], "up") +
 card("Limit-downs", s["跌停"], "down") +
 card("Up share", s["上涨占比"]) +
 card("Turnover", m ? m[1] + " (100M CNY)" : "—");

 document.getElementById("ts").textContent =
 "Updated " + new Date().toLocaleTimeString();
}

loadMood();
setInterval(loadMood, 30000); // 30s; never below 12s
</script>
</html>
```

## ⚠️ Three real pitfalls

-
 **Field names are Chinese** — the keys in `structured[0]` are literally `"上涨"`, `"下跌"`, `"涨停"`, not `up` / `down`. Generated code guesses English almost every time; always check against the live response.

-
 **上涨占比 is a string like "20%" , not the number 20** — it carries a percent sign. Run `parseFloat()` before any arithmetic, or your chart library gets NaN.

-
 **Turnover is not in structured** — `structured` only holds the first table (advancers/decliners). Turnover sits in the Markdown `data` text, so extract it with a regex: `b.data.match(/两市成交额：([\d.]+)亿/)`.

## Step 3: add individual stocks with one more endpoint

 JavaScript (same script block)
```
async function loadStock(code) {
 var r = await fetch(API + "/quote?code=" + code);
 var b = await r.json();
 if (!b.ok || !b.data || !b.data.length) return null;

 var bar = b.data[0]; // newest first, data[0] is the latest bar
 return {
 date: bar.date,
 last: Number(bar.last),
 high: Number(bar.high),
 low: Number(bar.low),
 volume: Number(bar.volume), // lots
 amount: Number(bar.amount), // CNY
 turnover: Number(bar.turnover) // percent
 };
}

loadStock("sh600519").then(console.log);
```
 |
| | Field | Meaning | Unit | Sample value (sh600519 · intraday 2026-10-29)

| | `date` | Trading date | — | 2026-10-29

| | `last` | Latest price | CNY | 1344.93

| | `high` / `low` | Session high / low | CNY | 1354.98 / 1322.60

| | `volume` | Volume | **lots** | 8251

| | `amount` | Turnover value | **CNY** | 1105305033

| | `turnover` | Turnover rate | % | 0.07

 `date`
 Trading date
 —
 2026-10-29

 `last`
 Latest price
 CNY
 1344.93

 `high` / `low`
 Session high / low
 CNY
 1354.98 / 1322.60

 `volume`
 Volume
 **lots**
 8251

 `amount`
 Turnover value
 **CNY**
 1105305033

 `turnover`
 Turnover rate
 %
 0.07

 `volume` is in lots (1 lot = 100 shares) and `amount` is in CNY — the two most common unit mistakes. Worth a code comment.

## Three things to watch once it runs

-
 **Anonymous limit is 5 requests per minute** — keep the refresh interval at 12 seconds or more, or you will get 429s. The 30 seconds above is safe.

-
 **Never hardcode a paid key in the frontend** — page source is public and the key will be copied. For higher rates, fetch on a server and keep the key there.

-
 **A dashboard describes state, it does not predict direction** — 4,156 advancers means "broad gains today", not "buy tomorrow". Read sentiment alongside historical averages and volume.

## What this setup is not good for

-
 **Sub-second refresh** — the free quota is 5/min; aggressive polling just gets rate limited. Use a key and aggregate server-side if you need more.

-
 **A multi-user product** — every visitor shares your single anonymous quota, so they crowd each other out.

-
 **Trading signals** — this article covers data retrieval and display only, not investment advice.

## FAQ
 Will a ChatGPT-generated page be blocked by CORS?
 **No.** The free endpoints return `access-control-allow-origin: *` (verified 2026-10-29), so a browser `fetch` works. The one-line check is in step 1.

 Do I need to sign up or apply for a key?
 **No.** `changedist` / `quote` / `hot` are among the 7 free endpoints — no signup, no key, anonymous calls allowed (5/min).

 Can I share the finished page?
 The site URL is shareable, in the form ` . .chatgpt.site`. **Anonymous access depends on your site settings in ChatGPT Sites** — open it in a private window to check: a sign-in prompt means the site is not public yet.

 Why does my advance/decline count differ from other platforms?
 Check the basis first: `/v1/changedist` is the **current session**; `/v1/market-overview?type=updown` is **T-1** (its response states the data date). Platforms also differ on scope (ST stocks, Beijing exchange, suspended names).

 Can I add a candlestick chart?
 Yes. `/v1/kline?code=sh600519&count=60` returns daily bars (free, forward-adjusted); feed them to any chart library. See the `kline` entry in the endpoint list for fields and parameters.

 Last updated: 2026-10-28
 Code and fields taken from live responses (re-verified 2026-10-29): `/v1/changedist` uses Chinese keys in `structured[0]` (上涨 / 下跌 / 涨停 / 跌停 / 上涨占比 / 停牌 / 平盘), `上涨占比` is a percent-sign string, and the two-market turnover appears only in the `data` text; `/v1/quote` returns `data` in reverse-date order with `volume` in lots and `amount` in CNY; both endpoints return `access-control-allow-origin: *`. Figures in this article are samples and change with the market; fields and rate limits follow the endpoint reference.

 Related

-
[Endpoint list (96 endpoints incl. changedist / quote / hot)](/en/endpoints)

-
[Quantifying A-Share market mood (all-free endpoints)](/en/docs/guides/market-mood)

-
[Getting A-share quotes in Python: 3 approaches](/en/docs/guides/python-ashare-quotes)

-
[Error codes & rate limits](/en/docs/errors)

 [← All tutorials](/en/docs/guides)
