> ## 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.

# Operator Outcomes

> Not in scope for trial — outcomes endpoint reference

<Note>
  **Not in scope for trial.** This outcomes endpoint is reference material for later integration. Daily operator outcomes may be shared separately by agreement; they are not required through this endpoint for the current trial.
</Note>

## Supporting outcome data

DCinside said that outcomes for **operator-placed posts can flow daily during rehearsal**. The model does not place posts at this stage. Outcomes are useful supporting data, but DCinside did not list their delivery as a rehearsal pass condition.

The endpoint and field mapping below are retained for later review, outside the current trial integration scope. The formal live-trial choice between sending events as they occur and hourly batches can be settled later.

## Requested data

* Board counts at 1, 6, 24 and 48 hours after placement: views, up-votes, down-votes and comments.
* Hide times and deletion events, when available.
* Who picked the post: operator or model version.
* Score at placement, when available, together with the original post identity.

The 1/6/24/48-hour ages are **measurement times**, not upload frequencies. Daily rehearsal uploads can carry readings from several measurement times. Proposed placement-baseline fields support interpreting net board growth and need payload confirmation.

## Updates and current implementation

`placement_id` identifies a placement. The current handler replaces the stored placement record on a repeated ID; it does **not** merge partial readings automatically. For current smoke tests, send the complete known snapshot for that placement. Agree snapshot-versus-incremental semantics before integration.

The current request protection is 5,000 records, not the 1,000 previously written in this document. It is an engineering limit, not an agreed business quota. Storage is process-local and must not be treated as durable outcome delivery.

The schema supports model-picked records for later publishing stages. Rehearsal operator outcomes use `picked_by.type = operator`; hypothetical model picks are logged separately on DCinside's side and are not actual placements.


## OpenAPI

````yaml openapi.json POST /v1/outcomes
openapi: 3.0.3
info:
  title: DCinside RTB API
  description: >-
    Proposed contract for DCinside's scoring-only dress rehearsal. Real queue
    candidates go to Surf; DCinside logs would-be picks while operators continue
    publishing. The model does not publish during rehearsal. The intended model
    is the delivered Model 0924 with down-vote weight 0; the deployed
    transport-test adapter is not that artifact. Five-minute cycles, inline
    responses, pending re-scoring, payload details and authentication are Surf
    proposals for confirmation. The response deadline is not yet agreed.
    DCinside's draft first-score target is at least 99% within two cycles of
    queue entry, with no separate scoring wait. This specification describes
    target rehearsal behavior, not deployment readiness. See the rehearsal
    documentation for implementation gaps and acceptance criteria.
  version: 1.1.0-draft
servers:
  - url: https://{host}
    description: Issued by Surf together with the API key.
    variables:
      host:
        default: rtb.example.com
        description: >-
          Host name issued by Surf for the environment (rehearsal or
          production). The default is a placeholder.
security:
  - ApiKey: []
tags:
  - name: Scoring
    description: >-
      Proposed cycle scoring and retrieval for rehearsal; cadence, refresh
      policy and deadline need confirmation.
  - name: Images
    description: >-
      Proposed one-time image transfer; layout and scoring readiness behavior
      need confirmation.
  - name: Outcomes
    description: >-
      Not in scope for trial. Reference for later outcomes-endpoint integration.
      Daily operator outcomes may be shared separately by agreement.
  - name: Weights
    description: >-
      Not in scope for trial. Reference for later client-facing
      weight-management integration. Surf configures the initial trial model and
      weights.
  - name: Health
    description: >-
      Service information; not proof of model readiness, durable state or alert
      delivery.
paths:
  /v1/outcomes:
    post:
      tags:
        - Outcomes
      summary: Send Outcomes
      description: >-
        Not in scope for trial. Reference endpoint for later integration:
        1/6/24/48-hour board readings, hides, deletions, picker and score at
        placement. Daily operator outcomes may be shared separately by agreement
        during rehearsal; this endpoint is not a required trial call. Sampling
        ages differ from upload frequency. Payload and live-trial
        event-versus-hourly delivery remain to be agreed. The current handler
        replaces a placement record instead of merging partial readings, so
        current tests must send complete known snapshots. Storage is
        process-local.
      operationId: postOutcomes
      parameters:
        - name: Content-Encoding
          in: header
          required: false
          schema:
            type: string
            enum:
              - gzip
          description: >-
            Optional compression. Set to gzip only for a gzip-compressed body;
            plain JSON is also supported.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OutcomesRequest'
            example:
              outcomes:
                - placement_id: example-4390001
                  gall_id: example_gallery
                  post_no: 31899120
                  tier: main
                  placed_at: '2026-09-29T02:40:00+09:00'
                  picked_by:
                    type: operator
                  score_at_placement: 0.62
                  views_at_placement: 1875
                  up_at_placement: 40
                  down_at_placement: 3
                  comment_at_placement: 21
                  readings:
                    - age_h: 1
                      observed_at: '2026-09-29T03:40:00+09:00'
                      views: 9120
                      up: 66
                      down: 8
                      comment_cnt: 30
                  hidden_at: null
                  deleted_at: null
                  deleted_by: null
      responses:
        '200':
          description: Stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutcomesResponse'
              example:
                received: 1
                created: 1
                updated: 0
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/TooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '500':
          $ref: '#/components/responses/ServerError'
components:
  schemas:
    OutcomesRequest:
      type: object
      properties:
        outcomes:
          type: array
          maxItems: 5000
          items:
            $ref: '#/components/schemas/OutcomeRecord'
          description: >-
            One record per placement. Current 5,000-record protection is an
            engineering limit, not an agreed quota. Daily operator-placed
            outcomes support rehearsal.
      required:
        - outcomes
    OutcomesResponse:
      type: object
      properties:
        received:
          type: integer
          minimum: 0
          example: 1
          description: Records in the request.
        created:
          type: integer
          minimum: 0
          example: 1
          description: Records stored as new placements.
        updated:
          type: integer
          minimum: 0
          example: 0
          description: Records that matched an existing `placement_id` and updated it.
      required:
        - received
        - created
        - updated
    OutcomeRecord:
      type: object
      properties:
        placement_id:
          type: string
          minLength: 1
          example: example-4390001
          description: >-
            DCinside placement identifier (delivered selected_id). Current
            implementation replaces the stored record on the same ID; send a
            complete known snapshot until incremental merging is agreed and
            implemented.
        gall_id:
          type: string
          minLength: 1
          description: Post key, together with `post_no`.
          example: example_gallery
        post_no:
          type: integer
          minimum: 1
          description: >-
            Post number. Unique only within a gallery, so a post is always keyed
            by `gall_id` and `post_no` together.
          example: 31900001
        tier:
          type: string
          enum:
            - main
            - light
            - night
            - app
          example: main
          description: 'The board the post was placed on. Delivered files: `rtb_head_tag`.'
        placed_at:
          type: string
          format: date-time
          description: >-
            When the post went onto the board. Also fixes the day and time block
            used for outcome percentiles. Delivered files: `exposed_at`.
          example: '2026-09-29T02:40:00+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        picked_by:
          oneOf:
            - $ref: '#/components/schemas/PickedByOperator'
            - $ref: '#/components/schemas/PickedByModel'
          description: >-
            Who chose the post: `{"type": "operator"}`, or `{"type": "model",
            "model_version", "weights_version", "cycle_id"}`.
        score_at_placement:
          type: number
          nullable: true
          example: 0.62
          description: >-
            The publish score the post had when placed. Lets us compare model
            and operator picks on the same board.
        views_at_placement:
          type: integer
          minimum: 0
          example: 1875
          description: >-
            Exact, from the board's `origin_hit`. Net views are readings minus
            this. Delivered files: `rtb_start_counts.views_at_placement`.
        up_at_placement:
          type: integer
          minimum: 0
          description: Last gallery-side up-votes before placement.
          example: 40
          nullable: true
        down_at_placement:
          type: integer
          minimum: 0
          description: Last gallery-side down-votes before placement.
          example: 3
          nullable: true
        comment_at_placement:
          type: integer
          minimum: 0
          description: Last gallery-side comment count before placement.
          example: 21
          nullable: true
        readings:
          type: array
          maxItems: 4
          items:
            $ref: '#/components/schemas/Reading'
          description: >-
            Known readings at 1, 6, 24 and 48 hours after placement. Daily
            rehearsal delivery may include several ages. On current
            repeated-placement updates include all known readings, because the
            handler replaces rather than merges.
        hidden_after_exposure:
          type: boolean
          nullable: true
          example: true
          description: >-
            The label DCinside has today: the post was hidden after it was
            exposed. Send it now, and use `hidden_at` once timed hides exist.
        hidden_at:
          type: string
          format: date-time
          description: >-
            When the post was taken down. DCinside's admin does not record this
            today, so send `null` until timed hides are logged through the
            unpublish path.
          example: '2026-09-29T05:00:00+09:00'
          nullable: true
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        deleted_at:
          type: string
          format: date-time
          description: >-
            When the post was deleted, if it was. Delivered files:
            `deletion.deleted_at`.
          example: '2026-09-29T06:00:00+09:00'
          nullable: true
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        deleted_by:
          type: string
          enum:
            - author
            - admin
            - auto
            - null
          nullable: true
          example: null
          description: >-
            Who deleted it: `author`, `admin` or `auto`. Delivered files:
            `deletion.deleted_by`.
        deleted_relative:
          type: string
          enum:
            - before_decision
            - after_decision
            - null
          nullable: true
          example: null
          description: >-
            Whether the deletion came before or after the operator's decision.
            Only `after_decision` is an outcome of the placement. Delivered
            files: `deletion.deleted_relative`.
      required:
        - placement_id
        - gall_id
        - post_no
        - tier
        - placed_at
        - picked_by
        - views_at_placement
        - readings
      description: One placement of a post on a board, with its outcome readings.
    Error:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
      required:
        - error
    PickedByOperator:
      type: object
      properties:
        type:
          type: string
          enum:
            - operator
          example: operator
          description: The post was chosen by an operator.
      required:
        - type
      description: Chosen by an operator.
    PickedByModel:
      type: object
      properties:
        type:
          type: string
          enum:
            - model
          example: model
          description: The post was chosen from the model's ranking.
        model_version:
          type: string
          example: m0924-weight0
          description: '`model_version` of the scores the choice was made from.'
        weights_version:
          type: string
          example: w-1
          description: '`weights_version` of those scores.'
        cycle_id:
          type: string
          pattern: ^\d{8}T\d{4}$
          example: 20260929T0240
          description: The cycle whose response the choice was made from.
      required:
        - type
        - model_version
        - weights_version
        - cycle_id
      description: Chosen from the model's ranking.
    Reading:
      type: object
      properties:
        age_h:
          type: integer
          enum:
            - 1
            - 6
            - 24
            - 48
          example: 1
          description: 'Hours since placement: 1, 6, 24 or 48.'
        observed_at:
          type: string
          format: date-time
          description: When the counters were read.
          example: '2026-09-29T03:40:00+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        views:
          type: integer
          minimum: 0
          description: Cumulative views on the board copy.
          example: 9120
          nullable: true
        up:
          type: integer
          minimum: 0
          description: Cumulative up-votes on the board copy.
          example: 66
          nullable: true
        down:
          type: integer
          minimum: 0
          description: Cumulative down-votes on the board copy.
          example: 8
          nullable: true
        comment_cnt:
          type: integer
          minimum: 0
          description: Cumulative comments on the board copy.
          example: 30
          nullable: true
      required:
        - age_h
        - observed_at
        - views
        - up
        - down
        - comment_cnt
      description: >-
        Board-copy counters sampled at 1, 6, 24 or 48 hours after placement.
        Placement baselines support net-growth calculations. These are sampling
        ages, not upload frequencies; outcome and training-label semantics need
        payload review.
    ErrorBody:
      type: object
      properties:
        code:
          type: string
          example: bad_request
          description: Machine-readable code. See Errors.
        message:
          type: string
          example: 2 fields failed validation
          description: Human-readable summary.
        fields:
          type: array
          items:
            $ref: '#/components/schemas/ErrorField'
          description: Every failing field, when the error is about the body.
      required:
        - code
        - message
    ErrorField:
      type: object
      properties:
        path:
          type: string
          example: candidates[0].created_at
          description: Path of the failing field.
        problem:
          type: string
          example: timestamp must include an explicit offset
          description: Description of the validation failure.
      required:
        - path
        - problem
  responses:
    BadRequest:
      description: >-
        Malformed JSON or a request-schema failure. Semantic checks may return
        only the first error; no exhaustive-error guarantee.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: bad_request
              message: 2 fields failed validation
              fields:
                - path: candidates[0].created_at
                  problem: timestamp must include an explicit offset
                - path: engagement[3].views
                  problem: must be a non-negative integer or null
    Unauthorized:
      description: The API key is missing or unknown.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: API key missing or unknown
    TooLarge:
      description: >-
        Current implementation size protection: 16 MiB JSON after decompression
        or 5 MiB per image. These are engineering protections, not agreed client
        quotas.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: too_large
              message: JSON body exceeds 16 MiB after decompression
    Unprocessable:
      description: >-
        The body is valid JSON and matches the schema, but the values are not
        acceptable. Nothing is stored.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unprocessable
              message: cycle_time cannot be in the future
              fields:
                - path: cycle_time
                  problem: must not be in the future
    ServerError:
      description: >-
        Server failure. Skip late or unusable scores for the decision cycle,
        preserve unacknowledged data and reconcile delivery. Alert delivery must
        be verified separately.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: server_error
              message: internal error
  securitySchemes:
    ApiKey:
      type: apiKey
      name: Authorization
      in: header
      description: >-
        Proposed API key sent as the whole Authorization header value.
        Long-lived credential scope, handoff and rotation need confirmation.
        Per-endpoint scopes are not currently enforced.

````