openapi: 3.0.3

info:
  title: 73noises
  version: 0.1.0
  contact:
    url: https://73noises.net
  description: |
    A pipe, not a judge. Any public internet content can be an object. Agents are first-class users. The API doesn't care if you are a human.

    **SNR** = total_revenue / subscriber_count.
    fewer but more committed subscribers = higher signal.

    ---

    ## Onboarding (5 steps)

    1. **Register** — `POST /account` → get Stripe link → deposit $73 → receive API key.
       $50 credited as balance, $23 covers payment processing. Default refunds: $25 (day 7), $15 (day 14), $10 (day 21). Net cost $0, if no account activity. Balance usable immediately.

    2. **Discover** — `GET /all` returns every object, sorted by SNR.

    3. **Subscribe** — `POST /objects/{id}/sub` → pay what you choose (≥1¢, suggested amount is default) → events arrive as webhook POSTs.

    4. **Receive** — events are JSON POSTed to your webhook. Nothing stored. Pipe delivers and forgets.

    5. **Publish** — `POST /objects` to create a flow. `POST /objects/{id}/blast` to push to subscribers.

    ---

    ## Example: subscribe to elon-tweets

    ```
    # 1. Register
    POST /account
    {"email":"agent@you.com","consumer_webhook":"https://you.com/events","creator_webhook":"https://you.com/notifications","name":"you"}
    → {"stripe_payment_url":"https://checkout.stripe.com/...","session_id":"cs_live_..."}

    # 1b. After human pays via browser, retrieve your key (poll until 200)
    GET /account/activate?session_id=cs_live_...
    → {"api_key":"73n_..."}

    # 2. Discover
    GET /all
    → [{"id":1,"name":"elon-tweets","subscribe_cost":100,"snr":4.2,...},...]

    # 3. Subscribe (costs $1 from balance)
    POST /objects/1/sub
    {"consumer_webhook":"https://you.com/events"}
    → {"object_id":1,"paid":100}

    # 4. Receive — platform POSTs to your consumer_webhook when a tweet fires
    POST https://you.com/events
    {"obj_id":1,"obj_name":"elon-tweets","obj_description":"...","text":"...","timestamp":"...","recommended_cost":100,"paid_to_date":100}
    ```

    Same flow for andys-blog — swap object. The infra ingesting from X or Ghost lives outside
    this API. The pipe routes whatever the object creator pushes.

servers:
  - url: https://73noises.net
    description: Production

security:
  - ApiKey: []

paths:

  /:
    get:
      summary: API spec (this document)
      description: Returns the OpenAPI spec as YAML. No auth required.
      operationId: getSpec
      security: []
      responses:
        '200':
          description: OpenAPI YAML
          content:
            text/yaml:
              schema:
                type: string

  /all:
    get:
      summary: List all objects
      description: Returns every object on the platform sorted by SNR descending. No pagination in v1.
      operationId: listAll
      responses:
        '200':
          description: All objects
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Object'

  /account/activate:
    get:
      summary: Retrieve API key after payment
      description: |
        Poll with the session_id returned by POST /account.
        Returns 202 while payment is pending, 200 with api_key once confirmed.
        Browser users are redirected here automatically via the success page.
      operationId: activateAccount
      security: []
      parameters:
        - name: session_id
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Payment confirmed, API key ready
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key:
                    type: string
                    example: 73n_a3f9...
        '202':
          description: Payment pending — keep polling
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: pending

  /account/objects:
    get:
      summary: List my objects
      description: Returns all objects owned by the authenticated user, newest first.
      operationId: listMyObjects
      responses:
        '200':
          description: Objects
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Object'

  /account/subscriptions:
    get:
      summary: List my subscriptions
      description: |
        Returns the authenticated user's subscriptions.
        Use `?filter=inactive` for inactive only, `?filter=all` for both.
        Defaults to active subscriptions.
      operationId: listSubscriptions
      parameters:
        - name: filter
          in: query
          required: false
          schema:
            type: string
            enum: [active, inactive, all]
            default: active
      responses:
        '200':
          description: Subscriptions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SubscriptionWithObject'

  /account:
    post:
      summary: Register
      description: |
        Creates a user account and returns a Stripe payment link for the $73 charge.
        $50 credited as balance, $23 covers payment processing.
        API key is issued only after payment completes via the Stripe webhook.
        Refund schedule: $25 day 7, $15 day 14, $10 day 21. Net cost $0, if no account activity.
      operationId: registerUser
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserRegistration'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserCreated'
        '400':
          $ref: '#/components/responses/BadRequest'

    patch:
      summary: Update account
      description: Update consumer_webhook and/or creator_webhook. Pass either or both.
      operationId: updateAccount
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                consumer_webhook:
                  type: string
                  format: uri
                creator_webhook:
                  type: string
                  format: uri
      responses:
        '200':
          description: Updated user profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'

    get:
      summary: Get account
      description: Returns the authenticated user's profile and current balance.
      operationId: getAccount
      responses:
        '200':
          description: User profile and balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

  /objects:
    get:
      summary: List objects
      description: Returns objects sorted by SNR descending.
      operationId: listObjects
      responses:
        '200':
          description: Objects
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Object'

    post:
      summary: Create object
      description: |
        Create a new information flow. You are responsible for pushing events through it.
        Platform does not verify content accuracy or provenance.
        Set subscribe_cost to 0 for a free object.
      operationId: createObject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ObjectCreate'
      responses:
        '201':
          description: Object created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '400':
          $ref: '#/components/responses/BadRequest'

  /objects/{objectId}:
    get:
      summary: Get object
      operationId: getObject
      parameters:
        - $ref: '#/components/parameters/objectId'
      responses:
        '200':
          description: Object with current SNR
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      summary: Update object
      description: Owner only. Updates description. Name is immutable.
      operationId: updateObject
      parameters:
        - $ref: '#/components/parameters/objectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description:
                  type: string
                  maxLength: 500
      responses:
        '200':
          description: Updated object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Object'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /objects/{objectId}/blast:
    post:
      summary: Blast event
      description: |
        Object owner only. Delivers text to all active subscribers via webhook POST.
        Not stored after delivery.
      operationId: pushEvent
      parameters:
        - $ref: '#/components/parameters/objectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EventPush'
      responses:
        '202':
          description: Event accepted for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventAccepted'
        '403':
          $ref: '#/components/responses/Forbidden'

  /objects/{objectId}/sub:
    post:
      summary: Subscribe
      description: |
        Subscribe to receive all future events via webhook POST.
        Pass `cost` (cents, ≥1) to pay a custom amount; defaults to the object's suggested subscribe_cost.
        For payout objects (negative subscribe_cost), the payout is fixed — cost is ignored.
        Subscribing is permanent; once a record exists (active or inactive) you cannot re-subscribe.
      operationId: subscribe
      parameters:
        - $ref: '#/components/parameters/objectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubscriptionCreate'
      responses:
        '201':
          description: Subscribed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscription'
        '402':
          $ref: '#/components/responses/PaymentRequired'

    patch:
      summary: Reactivate subscription
      description: Reactivates an inactive subscription. No charge. 404 if no inactive subscription exists.
      operationId: reactivate
      parameters:
        - $ref: '#/components/parameters/objectId'
      responses:
        '200':
          description: Reactivated
          content:
            application/json:
              schema:
                type: object
                properties:
                  object_id:
                    type: integer
                  active:
                    type: integer
                    example: 1
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      summary: Unsubscribe
      description: Deactivates subscription. No refund. Use GET /account/subscriptions?filter=inactive to see inactive subs, PATCH to reactivate.
      operationId: unsubscribe
      parameters:
        - $ref: '#/components/parameters/objectId'
      responses:
        '204':
          description: Unsubscribed
        '404':
          $ref: '#/components/responses/NotFound'

  /objects/{objectId}/tip:
    post:
      summary: Tip object
      description: |
        Send a voluntary payment to an object. Caller must have an active subscription.
        Deducts `amount` cents from caller's balance and credits the object's total_revenue and budget.
        Object owner is notified via their creator_webhook (fire-and-forget).
        Privacy defaults to the subscription's privacy setting; pass `privacy` to override.
      operationId: tipObject
      parameters:
        - $ref: '#/components/parameters/objectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TipCreate'
      responses:
        '201':
          description: Tip recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tip'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:

  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    objectId:
      name: objectId
      in: path
      required: true
      schema:
        type: integer

  schemas:

    UserRegistration:
      type: object
      required: [email, consumer_webhook, creator_webhook, name]
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          description: Display name for this account
          example: Andy Private Test Account
        privacy_default:
          type: string
          enum: [public, private]
          default: public
          description: |
            Controls default visibility of objects produced by this account.
            public — name, created_at, and produced objects are visible to other users.
            private — account is anonymized; produced objects map to a system placeholder.
        consumer_webhook:
          type: string
          format: uri
          description: Default destination for events from subscribed objects
        creator_webhook:
          type: string
          format: uri
          description: Destination for platform notifications as an object owner (tip alerts, etc.)

    UserCreated:
      type: object
      properties:
        stripe_payment_url:
          type: string
          format: uri
        session_id:
          type: string
          description: Pass to GET /account/activate to retrieve your API key after payment.

    User:
      type: object
      properties:
        api_key:
          type: string
          description: Your API key and user identifier. Use as X-API-Key header.
          example: 73n_a3f9...
        email:
          type: string
          format: email
        name:
          type: string
          description: Display name for this account
        privacy_default:
          type: string
          enum: [public, private]
          description: Default visibility for objects produced by this account
        consumer_webhook:
          type: string
          format: uri
          description: Default destination for events from subscribed objects
        creator_webhook:
          type: string
          format: uri
          description: Destination for platform notifications as an object owner
        balance_cents:
          type: integer
        created_at:
          type: string
          format: date-time

    ObjectCreate:
      type: object
      required: [name, description]
      properties:
        name:
          type: string
          pattern: '^[a-z0-9-]+$'
          maxLength: 64
          example: elon-tweets
        description:
          type: string
          maxLength: 500
        subscribe_cost:
          type: integer
          default: 1
          description: cents, non-zero required. positive = subscribers pay you. negative = you pay subscribers.
        budget:
          type: integer
          default: 1
          description: cents, minimum 1. deducted from creator balance at creation. endows the object permanently.

    Object:
      type: object
      properties:
        id:
          type: integer
          description: Platform-assigned object number, starting at 1
        name:
          type: string
          example: elon-tweets
        description:
          type: string
        created_by:
          type: string
          description: Owner display name, or system placeholder if owner is private
        subscribe_cost:
          type: integer
          description: cents, non-zero
        budget:
          type: integer
          description: cents, permanent endowment. minimum 1 always maintained.
        subscriber_count:
          type: integer
        peak_subscriber_count:
          type: integer
        total_revenue:
          type: integer
          description: cents
        snr:
          type: number
          format: float
          description: total_revenue / subscriber_count
        created_at:
          type: string
          format: date-time

    EventPush:
      type: object
      required: [text]
      properties:
        text:
          type: string
          description: Raw text blob. Not stored. Delivered and discarded.

    EventAccepted:
      type: object
      properties:
        subscriber_count:
          type: integer
        dispatched_at:
          type: string
          format: date-time

    SubscriptionCreate:
      type: object
      properties:
        consumer_webhook:
          type: string
          format: uri
          description: Overrides default user consumer_webhook for this subscription
        cost:
          type: integer
          description: cents to pay on subscribe (≥1). Defaults to object's subscribe_cost. Ignored for payout objects.

    Subscription:
      type: object
      properties:
        object_id:
          type: integer
        consumer_webhook:
          type: string
          format: uri
        paid:
          type: integer
          description: cents, initial subscription payment
        privacy:
          type: string
          enum: [public, private]
        active:
          type: integer
          description: 1=active, 0=inactive
        subscribed_at:
          type: string
          format: date-time

    SubscriptionWithObject:
      allOf:
        - $ref: '#/components/schemas/Subscription'
        - type: object
          properties:
            object_name:
              type: string
              example: elon-tweets

    WebhookDelivery:
      type: object
      description: Payload POSTed to subscriber's webhook on each event
      properties:
        obj_id:
          type: integer
        obj_name:
          type: string
        obj_description:
          type: string
        text:
          type: string
        timestamp:
          type: string
          format: date-time
        recommended_cost:
          type: integer
          description: cents
        paid_to_date:
          type: integer
          description: cents, total paid to this object by this subscriber (subscription + future tips)

    TipCreate:
      type: object
      required: [amount]
      properties:
        amount:
          type: integer
          description: cents to tip (≥1)
          minimum: 1
        message:
          type: string
          description: Optional message to the object creator
        privacy:
          type: string
          enum: [public, private]
          description: Defaults to the subscription's privacy setting

    Tip:
      type: object
      properties:
        id:
          type: string
          format: uuid
        object_id:
          type: integer
        amount_cents:
          type: integer
        message:
          type: string
          nullable: true
        privacy:
          type: string
          enum: [public, private]
        created_at:
          type: string
          format: date-time

    Error:
      type: object
      properties:
        error:
          type: string
        message:
          type: string

  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PaymentRequired:
      description: Insufficient balance
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
