> ## Documentation Index
> Fetch the complete documentation index at: https://surf-dcinside-api.kr.ask.surf/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> DCinside RTB API - publish scores for the Real-Time Best candidate pool

<h2 style={{ fontSize: "22px", marginBottom: 8 }}>About the RTB API</h2>

Real-Time Best (RTB) boards on DCinside are filled from a pool of candidate posts. The RTB API is how DCinside hands that pool to Surf and gets a **publish score** for every pending post back, once per cycle (five minutes by default). Surf builds the features, runs the model and applies the weights DCinside sets. DCinside applies its own hard gate and publishes.

Every call is made by DCinside, so DCinside only ever connects outward. Scores come back in the response to the cycle call itself. The trial candidates are the full admin extraction queue that operators review, about 2,200 posts a day. See [Rollout Stages](/rollout) for the dress rehearsal and the trial stages.

<Warning>
  **Status: proposal.** Version 1 of this API is Surf's proposal for the technical exchange with DCinside. Calls, field names and limits can change until both sides agree. The host name and API keys are issued for the rehearsal, and the host shown in examples, `rtb.example.com`, is a placeholder.
</Warning>

<h3 style={{ marginTop: 20 }}>Cadence & freshness</h3>

<CardGroup cols={2}>
  <Card title="One call per cycle" icon="clock">
    Every five minutes by default, on the boundaries `:00`, `:05`, `:10` and so on, Korean time. That is 288 cycles a day. DCinside's interval is a setting that starts at 2–5 minutes, and this proposal starts at five.
  </Card>

  <Card title="One score per post" icon="lock">
    A post is scored once, after a 15-minute wait, and the score is repeated unchanged in every later response until the post is removed.
  </Card>
</CardGroup>

***

<h2 style={{ fontSize: "22px", marginBottom: 8 }}>How a cycle works</h2>

<Steps>
  <Step title="DCinside calls at the cycle boundary">
    DCinside applies its 24-hour age filter, so posts older than a day are not sent and a pending candidate that crosses 24 hours gets a removal signal. It then makes one `POST /v1/cycles` call. The body carries new candidates (text sent once, images uploaded separately), the current engagement of every pending candidate, the admin candidate-score rows and the removals. The cycle id is the idempotency key.
  </Step>

  <Step title="Surf scores">
    We add the call to the stored series, build the features and score every candidate whose 15-minute wait is over. Scoring runs inside the request.
  </Step>

  <Step title="DCinside reads the response and publishes">
    The response has one row per pending candidate, with its status and publish score. DCinside passes the scores to its hard gate and publishes.
  </Step>

  <Step title="A lost cycle is not lost data">
    A retry with the same cycle id returns the stored response and never scores twice. If nothing usable arrives in time, DCinside skips that cycle, and `GET /v1/scores/{cycle_id}` still returns the stored result.
  </Step>
</Steps>

The cycle call, the read-back of a stored cycle, image upload and health are the MVP. Images are uploaded one by one in the background, never on the scoring path. Outcomes, meaning how placed posts performed, and the weights calls are documented for later and are **not in MVP scope**.

***

<h2 style={{ fontSize: "22px", marginBottom: 8 }}>The calls</h2>

| Call | Scope | When | What it does |
| - | - | - | - |
| [`POST /v1/cycles`](/cycles) | MVP | every cycle | Sends candidates, engagement, candidate scores and removals. Returns the scores. |
| [`GET /v1/scores/{cycle_id}`](/scores) | MVP | after a timeout or a lost response | Returns the stored response of a cycle. Scores nothing. |
| [`GET /v1/health`](/health) | MVP | any time | Returns service state and the last cycle received. |
| [`PUT /v1/images/{gall_id}/{post_no}/{NN}`](/images) | MVP | once per image, in the background | Uploads one image of a post. |
| [`POST /v1/outcomes`](/outcomes) | Not in MVP | daily in the dress rehearsal; live: as they occur or hourly, to be agreed | Sends how placed posts performed. |
| [`PUT /v1/weights`](/set-weights) | Not in MVP | when DCinside changes the weights | Sets the weights of the publish score. |
| [`GET /v1/weights`](/get-weights) | Not in MVP | any time | Returns the weights in force. |

<h2 style={{ fontSize: "22px", marginBottom: 8 }}>Who does what</h2>

<AccordionGroup>
  <Accordion title="DCinside">
    * Extracts candidates and applies the 24-hour age filter
    * Sends each post's text once and its images once, when it becomes a candidate
    * Makes every call, at every cycle boundary
    * Runs the hard gate on its publishing side: at most 7 posts of one category and 6 of one gallery in the last 20 on a tier, no more than 2 in a row from a gallery, one post per event, the safety filter and the publishing rate
    * Later, not in MVP scope: sets the weights of the publish score and sends outcomes for the posts it placed
  </Accordion>

  <Accordion title="Surf">
    * Builds the features and runs the model inside each cycle call
    * Keeps the engagement series and the outcome store
    * Returns the scores, versions and counts with every response
    * Watches the service and alerts in Slack if no cycle call arrives for 15 minutes
    * Later, not in MVP scope: retrains the model from the outcomes
  </Accordion>
</AccordionGroup>

<Card title="Ready to get started?" icon="rocket" href="/conventions">
  Start with the conventions every call shares, then open Score a Cycle.
</Card>
