# Tokensensus Publisher API · v1

Base URL: https://tokensensus.vercel.app
Studio: https://tokensensus.vercel.app/publishers

## 1. Receive a publisher key

Publisher keys are issued privately per newsroom and scoped to registered article domains.
Keep the key on the CMS server. Never place it in JavaScript, an iframe URL, a Git repository, or an article's HTML.
Use Publisher studio to sign in and review your newsroom’s drafts.

## 2. Select and submit an article from the CMS

Opt in with a leading `[POLL]` or `[VOTE]` headline marker (case-insensitive), or `pollEnabled: true`. `pollEnabled: false` always skips. An unmarked article without the true flag returns `{ article: null, skipped: true, reason: "not_selected", reused: false }` with HTTP 200, no saved article and no AI call. The prefix is removed from the stored display title.

Selected articles are assessed for a suitable reader question. Eligible stories receive a draft for editorial review; unsuitable stories return a rejected status. Market-price stories are supported.

Call this after the full article body is available. This endpoint reads the submitted text; it does not crawl arbitrary URLs or bypass paywalls.

```http
POST /api/v1/articles
Authorization: Bearer YOUR_PRIVATE_PUBLISHER_KEY
Content-Type: application/json
```

```json
{
  "articleId": "your-stable-cms-id",
  "url": "https://news.example.com/articles/your-article",
  "title": "The article headline",
  "body": "Full plain-text article body, between 200 and 40,000 characters.",
  "language": "en",
  "pollEnabled": true,
  "imageUrl": "https://your-cdn.example/article-thumbnail.jpg"
}
```

`language`: `en` or `ko`. Use HTTPS and an exact registered origin. Send a canonical URL; tracking parameters are stripped.
Selected processing returns `{ article, reused }`. Unmarked stories return `article: null` and `skipped: true`. Optional `imageUrl` must be HTTPS; pass the original CMS thumbnail or og:image URL. `article.id` is the permanent Tokensensus question ID.

- `201`: generation completed; inspect `article.status` (`draft` or `rejected`). Never assume 201 means published.
- `200`: the same article already exists; no duplicate AI call.
- `202`: another request is generating this article; poll the authenticated GET endpoint.
- `409 article_changed`: an ID or URL already exists with different content. Do not overwrite an existing vote. Use a distinct ID and URL for an editorial revision.
- `429`: request allowance reached. Contact the operator for your newsroom’s limits and next available window.
- `502`: processing failed. Check the article status before retrying the same payload. Do not retry automatically on reader visits.
- `401/403`: invalid key or unregistered article domain.

Requests are limited to 180 KB. Allow up to 90 seconds for generation. AI suggestions include a source excerpt and require editorial review; they may be rejected when grounding or choices fail validation.

## 3. Review and publish

Removing a headline marker or setting `pollEnabled: false` skips further processing; it does not close an already-published question. Use the close action to end participation, or remove the embed container to stop displaying it on your site.

Use the studio, or the authenticated API:

```http
GET /api/v1/articles
GET /api/v1/articles/ARTICLE_POLL_ID
```

The detail response is `{ article }`, containing the article, question, source excerpt and publication status. These authenticated endpoints contain your newsroom’s article text and must remain private.

```http
PATCH /api/v1/articles/ARTICLE_POLL_ID
Authorization: Bearer YOUR_PRIVATE_PUBLISHER_KEY
Content-Type: application/json
```

```json
{
  "action": "publish",
  "reviewed": true,
  "question": {
    "title": "Which outcome matters most to you?",
    "description": "A short context grounded in the source article.",
    "category": "Technology",
    "format": "multiple",
    "options": ["Choice A", "Choice B", "Not sure yet"]
  }
}
```

Categories: `Technology`, `Crypto`, `Culture`, `Society`.
Formats: `multiple` (2–6 unique choices), `versus` (exactly 2 unique choices).
Only a draft can be published or rejected. After publishing, choices are immutable. Close voting with `{"action":"close"}`; existing results remain visible. Reject a draft with `{"action":"reject"}`.

## 4. Embed the published question

```html
<div data-tokensensus data-publisher="YOUR_PUBLISHER_ID" data-article-id="your-stable-cms-id" data-lang="en"></div>
<script src="https://tokensensus.vercel.app/embed.js" defer></script>
```

`data-lang` is `en` (default) or `ko` and controls widget labels and notices. Question language is set separately by ingestion `language`; existing choices are not automatically translated.

The publisher slug is public; it is not an API key. Use the same CMS article ID as in step 2.
If `data-article-id` is omitted, the script resolves `data-article-url`, then the page's canonical URL, then its current URL. It only reads that URL, not the article body or reader data.
Unpublished questions are hidden. The script never calls AI or submits article text. For phase 1, generation happens through the authenticated CMS API or studio only.

The iframe automatically reports its height. Message handlers check the service origin and exact iframe window.
If your site uses Content Security Policy, permit `https://tokensensus.vercel.app` in `script-src`, `frame-src`, and `connect-src`.
Install the script after its container or use `defer`. Repeat the container for additional questions. Call the script again after dynamically adding containers in a single-page application.

A direct iframe also works:

```html
<iframe src="https://tokensensus.vercel.app/embed/ARTICLE_POLL_ID"
        title="Reader vote" width="100%" height="650" style="border:0" loading="lazy"></iframe>
```

### Reuse one question across several articles

To share a published question, copy its existing embed to the other articles. Keep the **original** `data-publisher` and `data-article-id`, even when the host article or newsroom is different. Alternatively, reuse its direct iframe with the same `ARTICLE_POLL_ID`. These are public identifiers; no publisher API key is needed to display a public question.

```html
<div data-tokensensus data-publisher="ORIGINAL_PUBLISHER_ID"
     data-article-id="ORIGINAL_CMS_ARTICLE_ID" data-lang="en"></div>
<script src="https://tokensensus.vercel.app/embed.js" defer></script>
```

Use `data-lang="ko"` for Korean controls. The question and choices are not translated. Both widget languages use the same results when they resolve to the same poll ID.

An editor must check that the question, choices, timeframe and context fit every article. Do not submit another article just to display this existing question: reusing the embed does not generate a new question or call AI. It keeps the original publisher, thumbnail and source attribution. Additional host articles are not registered as sources and per-placement results are not available.

There is no similarity search, automatic merging or bilingual question-translation workflow yet. Independently generated questions keep separate IDs and results, even if their titles match. Publishing separate English and Korean questions creates separate polls; changing the widget language alone does not.

## 5. Results and rankings

`GET /api/public/polls/ARTICLE_POLL_ID` returns the published question and server totals. Drafts and article bodies are not public.
`GET /api/public/polls?language=en` lists up to 60 published/closed **English** questions. Use `?language=ko` for Korean questions. Omission defaults to English; other values return HTTP 400. Add `&publisher=SLUG` to filter a newsroom. Language filtering occurs before the 60-question limit. The response is `{ polls, language }` and includes publisher ID/name/logo, article thumbnail/source URL, question language, aggregate counts and verified embed status. Each query URL has its own cache entry for up to 30 seconds.

The website uses separate English and Korean catalogs on Home, Feed, Rankings and Newsrooms. Switching language changes the catalog, not question text or votes. An empty edition stays empty. Direct links and explicitly configured embeds can still open an existing question in its original language; the catalog setting is not an access restriction or automatic translation.
The Rankings page links to these shared results. Different questions remain separate; questions with different wording or choices are never merged automatically.

The widget sends votes to the same-origin API. The server validates status and option, and stores one vote per question and browser identifier atomically. Reloads and retries do not add another vote for that identifier. Server totals are refreshed on page load, after a vote, or via Refresh.

This is not one-person-one-vote: cleared storage, another browser, private mode, or third-party storage partitioning can create another identifier. Public participation is not a representative poll. Stronger bot/human verification and cross-site identity are future work. No blockchain transaction is currently performed.

## Integration security

- Keep your newsroom’s integration key on its CMS server and send it only to the Tokensensus API over HTTPS. Never share it between newsrooms.
- Contact the operator to revoke and replace a key if it is exposed.
- Submit only article text your newsroom is authorized to process. Questions require editorial review before publication.
- Check article status after a timeout before retrying the same payload.

## 6. Publisher logo and onboarding

`PATCH /api/v1/publisher` uses the same private authentication and accepts `{ "logoUrl": "https://your-cdn.example/logo.png" }` (or null to remove). Only your own publisher profile is changed. The logo and article thumbnail are public assets loaded from the supplied hosts; supply images your newsroom is authorized to display.

A platform operator verifies a newsroom’s domain, registers exact HTTPS origins, sets the generation quota and privately issues a distinct key. The same API works for every newsroom; public self-service key issuance is not implemented. The operator separately verifies a live embed installation before `embed_verified` is enabled. It is not inferred from the presence of a source URL.

## 7. Downloadable SDK

Node.js 20+: download `/sdk/tokensensus.mjs` and `/sdk/tokensensus.d.mts` into the same directory. See `/developers` and `/publisher-guide-ko.md`. This module is not published to npm. SDK methods: submitArticle, listArticles, getArticle, publish (explicit reviewed:true), reject, close, updateLogo. `embedCode` creates escaped public markup with no private key. No automatic retries are performed.

