openapi: 3.1.0
info:
  title: Strike a Block API
  version: "2026-09-10"
  summary: Genesis blocks as a service.
  description: |
    POST an inscription; the foundry strikes it into a real proof-of-work genesis
    block on a Bitmain BM1397 ASIC at Bitcoin's original difficulty (or deeper),
    publishes it to the public vault, and notifies you by signed webhook. Strikes
    are prepaid in credits (1 credit = US$1). Human-readable docs:
    https://strikeablock.com/docs.html
  contact:
    email: hello@strikeablock.com
  termsOfService: https://strikeablock.com/terms.html
servers:
  - url: https://strikeablock.com/api/v1
security:
  - apiKey: []
tags:
  - name: Strikes
  - name: Credits
  - name: Webhooks
  - name: Blocks
  - name: Account

paths:
  /pricing:
    get:
      tags: [Credits]
      summary: Tiers and prices
      security: []
      responses:
        "200":
          description: The pricing table.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pricing" }

  /strikes:
    post:
      tags: [Strikes]
      summary: Create a strike
      description: |
        Validates and moderates the inscription, charges the credits atomically and
        queues the strike for the ASIC. A 4xx never charges. Responds 202 with a
        `Location` header; poll it or wait for the webhook.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StrikeCreate" }
            examples:
              genesis:
                value: { text: "For Mom, 1958-2024", zeros: 8, webhook_url: "https://example.com/strikeablock", metadata: { order: "A-1042" } }
      responses:
        "202":
          description: Queued. Includes `credits_remaining` and `queue_ahead`.
          headers:
            Location: { schema: { type: string }, description: Path of the new strike. }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Strike" }
        "200":
          description: Replayed from an earlier request with the same Idempotency-Key.
          headers:
            Idempotent-Replayed: { schema: { type: string, enum: ["true"] } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Strike" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Not enough credits. `error.required` and `error.balance` say how many.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "409":
          description: "`duplicate_inscription` (already in the vault) or `queue_full` (25 pending)."
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "422":
          description: Content policy (`rejected_content` / `rejected_junk`).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Strikes]
      summary: List your strikes
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - name: status
          in: query
          schema: { type: string, enum: [queued, mining, done, rejected, failed] }
      responses:
        "200":
          description: Newest first. `block` is omitted in lists.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/List"
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: "#/components/schemas/Strike" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /strikes/{id}:
    get:
      tags: [Strikes]
      summary: Retrieve a strike
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: The strike, with `block` once done and `queue_ahead` while queued.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Strike" }
        "404": { $ref: "#/components/responses/NotFound" }

  /strikes/{id}/deliveries:
    get:
      tags: [Webhooks]
      summary: Webhook deliveries for one strike
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deliveries, newest first.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/List"
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: "#/components/schemas/WebhookDelivery" } }

  /credits:
    get:
      tags: [Credits]
      summary: Current balance
      responses:
        "200":
          description: Balance.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: balance }
                  credits: { type: integer }

  /credits/checkout:
    post:
      tags: [Credits]
      summary: Start a Stripe Checkout for credits
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [credits]
              properties:
                credits: { type: integer, minimum: 10, maximum: 1000, description: "1 credit = US$1." }
                success_url: { type: string, format: uri, description: "https URL; may contain {CHECKOUT_SESSION_ID}." }
                cancel_url: { type: string, format: uri }
      responses:
        "201":
          description: Send the buyer to `url`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CheckoutSession" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "503":
          description: Purchases not configured on this deployment.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  /credits/claim:
    post:
      tags: [Credits]
      summary: Apply a paid checkout session to your balance
      description: Idempotent. Also happens automatically via Stripe webhook and periodic reconciliation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session_id]
              properties:
                session_id: { type: string, pattern: "^cs_" }
      responses:
        "200":
          description: Outcome.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: credit_claim }
                  session_id: { type: string }
                  applied: { type: boolean }
                  reason: { type: [string, "null"], enum: [already_applied, unpaid, null] }
                  credits: { type: integer }
                  balance: { type: integer }
                  payment_status: { type: string }
        "403":
          description: The session belongs to another account.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  /credits/ledger:
    get:
      tags: [Credits]
      summary: Balance history
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
      responses:
        "200":
          description: Newest first.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/List"
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: "#/components/schemas/LedgerEntry" } }

  /webhook:
    get:
      tags: [Webhooks]
      summary: Account webhook configuration (includes the signing secret)
      responses:
        "200":
          description: Configuration.
          content: { application/json: { schema: { $ref: "#/components/schemas/Webhook" } } }
    put:
      tags: [Webhooks]
      summary: Set the account webhook URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, description: https only. }
                rotate_secret: { type: boolean, default: false }
      responses:
        "200":
          description: Configuration, with the (possibly new) secret.
          content: { application/json: { schema: { $ref: "#/components/schemas/Webhook" } } }
        "400": { $ref: "#/components/responses/BadRequest" }
    delete:
      tags: [Webhooks]
      summary: Remove the account webhook URL (keeps the secret)
      responses:
        "200":
          description: Configuration.
          content: { application/json: { schema: { $ref: "#/components/schemas/Webhook" } } }

  /webhook/test:
    post:
      tags: [Webhooks]
      summary: Queue a signed strike.test event
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri, description: Defaults to the account URL. }
      responses:
        "202":
          description: Queued for delivery within seconds.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: webhook_test }
                  delivery_id: { type: string }
                  url: { type: string }
                  event: { type: string, const: strike.test }
                  message: { type: string }

  /webhook/deliveries:
    get:
      tags: [Webhooks]
      summary: Recent deliveries across all strikes
      parameters:
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Newest first.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/List"
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: "#/components/schemas/WebhookDelivery" } }

  /blocks:
    get:
      tags: [Blocks]
      summary: Public blocks, newest first
      security: []
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
      responses:
        "200":
          description: Blocks.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/List"
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: "#/components/schemas/Block" } }

  /blocks/{block_id}:
    get:
      tags: [Blocks]
      summary: One public block
      security: []
      parameters:
        - { name: block_id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: The block.
          content: { application/json: { schema: { $ref: "#/components/schemas/Block" } } }
        "404": { $ref: "#/components/responses/NotFound" }

  /account:
    get:
      tags: [Account]
      summary: Your account
      responses:
        "200":
          description: Account.
          content: { application/json: { schema: { $ref: "#/components/schemas/Account" } } }

  /keys:
    get:
      tags: [Account]
      summary: List API keys (never the key itself)
      responses:
        "200":
          description: Keys.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/List"
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: "#/components/schemas/ApiKey" } }
    post:
      tags: [Account]
      summary: Create an API key (sign-in token only)
      security:
        - signInToken: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 60 }
      responses:
        "201":
          description: The key is in `key`, shown once.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiKey"
                  - type: object
                    properties:
                      key: { type: string, pattern: "^sab_live_" }
        "403":
          description: "`portal_only`: an API key cannot mint keys."
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "409":
          description: "`too_many_keys` (10 active)."
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  /keys/{id}:
    delete:
      tags: [Account]
      summary: Revoke an API key (sign-in token only)
      security:
        - signInToken: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Revoked.
          content: { application/json: { schema: { $ref: "#/components/schemas/ApiKey" } } }
        "404": { $ref: "#/components/responses/NotFound" }

webhooks:
  strike.done:
    post:
      summary: A strike landed
      description: |
        Delivered to the strike's `webhook_url` or the account webhook. Signed via
        `X-SAB-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`.
        Respond 2xx within 10 s; otherwise retried at 1m, 5m, 15m, 1h, 3h, 6h, 12h.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Event" }
      responses:
        "200": { description: Acknowledged. }
  strike.rejected:
    post:
      summary: A strike was refused at mining time (credits refunded)
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Event" }
      responses:
        "200": { description: Acknowledged. }
  strike.failed:
    post:
      summary: The search budget ran out (credits refunded)
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Event" }
      responses:
        "200": { description: Acknowledged. }
  strike.test:
    post:
      summary: Sample event from POST /webhook/test (livemode false)
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Event" }
      responses:
        "200": { description: Acknowledged. }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: sab_live_…
      description: Create keys at https://strikeablock.com/developers.html
    signInToken:
      type: http
      scheme: bearer
      bearerFormat: Firebase ID token
      description: Used by the developer console only.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 100 }
      description: Same key + same account replays the original strike without charging.
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    StartingAfter:
      name: starting_after
      in: query
      schema: { type: string }
      description: The `next_cursor` (an object id) from the previous page.

  responses:
    BadRequest:
      description: Validation failed; `error.param` names the field.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: Missing, unknown or revoked key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: No such resource (or not yours).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    RateLimited:
      description: 60 requests/minute per account.
      headers:
        Retry-After: { schema: { type: integer }, description: Seconds. }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          required: [type, code, message]
          properties:
            type:
              type: string
              enum: [invalid_request_error, authentication_error, insufficient_credits, moderation_error, not_found, rate_limit_error, api_error]
            code: { type: string }
            message: { type: string }
            param: { type: string }
          additionalProperties: true

    List:
      type: object
      properties:
        object: { type: string, const: list }
        data: { type: array, items: {} }
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"] }

    Pricing:
      type: object
      properties:
        object: { type: string, const: pricing }
        credit_usd: { type: number, const: 1.0 }
        purchase:
          type: object
          properties:
            min_credits: { type: integer }
            max_credits: { type: integer }
            currency: { type: string }
        tiers:
          type: array
          items:
            type: object
            properties:
              zeros: { type: integer, enum: [8, 10, 11, 12] }
              tier: { type: string, enum: [genesis, gold, platinum, prismatic] }
              credits: { type: integer }
              typical_time: { type: string }
              nbits: { type: string }
        text_max_chars: { type: integer }
        max_pending_strikes: { type: integer }
        refunds: { type: string }

    StrikeCreate:
      type: object
      required: [text]
      properties:
        text:
          type: string
          minLength: 3
          maxLength: 90
          description: The inscription. Whitespace is collapsed; control characters refused.
        zeros:
          type: integer
          enum: [8, 10, 11, 12]
          default: 8
        webhook_url:
          type: string
          format: uri
          description: https URL; overrides the account webhook for this strike.
        metadata:
          type: object
          maxProperties: 10
          additionalProperties:
            oneOf: [{ type: string, maxLength: 200 }, { type: number }, { type: boolean }]
        email:
          type: string
          format: email
          description: Receives the block certificate when the strike lands.

    Strike:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: strike }
        status: { type: string, enum: [queued, mining, done, rejected, failed] }
        text: { type: string }
        zeros: { type: integer }
        tier: { type: string, enum: [genesis, gold, platinum, prismatic] }
        credits_charged: { type: integer }
        refunded: { type: boolean }
        reason: { type: [string, "null"], enum: [content, junk, duplicate, invalid, null] }
        created_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        completed_at: { type: [string, "null"], format: date-time }
        queued_ms: { type: [integer, "null"] }
        mine_ms: { type: [integer, "null"] }
        attempts: { type: integer }
        queue_ahead: { type: integer, description: Only while queued (and on creation). }
        credits_remaining: { type: integer, description: Only on creation. }
        webhook_url: { type: [string, "null"] }
        metadata: { type: object }
        block_id: { type: [string, "null"] }
        block:
          oneOf: [{ $ref: "#/components/schemas/Block" }, { type: "null" }]
        url: { type: [string, "null"], format: uri }

    Block:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: block }
        text: { type: string }
        hash: { type: string, pattern: "^[0-9a-f]{64}$" }
        zeros: { type: integer, description: Depth requested. }
        zeros_deep: { type: integer, description: Leading zeros actually present. }
        nonce: { type: integer }
        nonce_hex: { type: string }
        nbits: { type: string }
        target: { type: string, pattern: "^[0-9a-f]{64}$" }
        timestamp: { type: integer }
        header_hex: { type: string, description: 80-byte header, hex. }
        coinbase_tx_hex: { type: string }
        merkle_root: { type: [string, "null"] }
        device: { type: [string, "null"] }
        mine_ms: { type: [integer, "null"] }
        mined_at: { type: [string, "null"], format: date-time }
        url: { type: string, format: uri }

    Event:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: event }
        type: { type: string, enum: [strike.done, strike.rejected, strike.failed, strike.test] }
        api_version: { type: string }
        created: { type: integer, description: Unix seconds. }
        livemode: { type: boolean }
        data:
          type: object
          properties:
            strike: { $ref: "#/components/schemas/Strike" }

    WebhookDelivery:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: webhook_delivery }
        strike_id: { type: [string, "null"] }
        event: { type: string }
        url: { type: string }
        state: { type: string, enum: [pending, delivered, failed] }
        attempts: { type: integer }
        next_attempt_at: { type: [string, "null"], format: date-time }
        last_attempt_at: { type: [string, "null"], format: date-time }
        delivered_at: { type: [string, "null"], format: date-time }
        last_status: { type: [integer, "null"] }
        last_error: { type: [string, "null"] }
        created_at: { type: [string, "null"], format: date-time }

    Webhook:
      type: object
      properties:
        object: { type: string, const: webhook }
        url: { type: [string, "null"] }
        secret: { type: [string, "null"], description: whsec_… used for X-SAB-Signature. }
        events: { type: array, items: { type: string } }
        signature_header: { type: string, const: X-SAB-Signature }
        updated_at: { type: [string, "null"], format: date-time }

    CheckoutSession:
      type: object
      properties:
        object: { type: string, const: checkout_session }
        id: { type: string }
        url: { type: string, format: uri }
        credits: { type: integer }
        amount_cents: { type: integer }
        currency: { type: string }
        expires_at: { type: [string, "null"], format: date-time }
        status: { type: string }

    LedgerEntry:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: ledger_entry }
        kind: { type: string, enum: [purchase, strike, refund, grant] }
        delta: { type: integer }
        balance_after: { type: [integer, "null"] }
        strike_id: { type: [string, "null"] }
        reference: { type: [string, "null"] }
        note: { type: [string, "null"] }
        created_at: { type: [string, "null"], format: date-time }

    ApiKey:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: api_key }
        name: { type: string }
        prefix: { type: string }
        created_at: { type: [string, "null"], format: date-time }
        last_used_at: { type: [string, "null"], format: date-time }
        revoked_at: { type: [string, "null"], format: date-time }

    Account:
      type: object
      properties:
        object: { type: string, const: account }
        id: { type: string }
        email: { type: [string, "null"] }
        credits: { type: integer }
        strikes_total: { type: integer }
        pending_strikes: { type: [integer, "null"] }
        webhook:
          type: object
          properties:
            url: { type: [string, "null"] }
            configured: { type: boolean }
        created_at: { type: [string, "null"], format: date-time }
        auth: { type: string, enum: [key, id_token] }
        pricing: { $ref: "#/components/schemas/Pricing" }
