openapi: 3.0.3
info:
  title: Black_Wall API
  description: |
    Pre-action outcome prediction for AI agents.

    Your AI agent has a twin. The twin tries it first.

    Black_Wall is a token-metered REST API. Send your agent's proposed action;
    we return a predicted outcome, a 0–100 risk score, a list of red flags,
    a reversibility class + rollback cost, and a GO / CAUTION / STOP recommendation
    in a few seconds — before the action runs.

    Free tier: ~100 forecasts/mo, no credit card. Get a key at https://blackwalltier.com
  version: 1.0.0
  contact:
    name: Black_Wall Support
    email: hello@forecast.dev
    url: https://blackwalltier.com

servers:
  - url: https://blackwalltier.com/api
    description: Production
  - url: http://localhost:3000/api
    description: Local development

security:
  - bearerAuth: []

paths:
  /v1/forecast:
    post:
      summary: Predict the outcome of a proposed action
      description: |
        Send a description of an action the agent is about to take.
        Receive a forecast: predicted outcome, risk score, red flags, GO/CAUTION/STOP.

        Token cost typically 50–200 per call (depth: standard), 250–800 (depth: deep).
        Charge is debited from the caller's account on success.

        Returns 402 if the account has insufficient tokens.
        Returns 429 if rate-limited.
      operationId: createForecast
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ForecastRequest"
            examples:
              email:
                summary: Email send check
                value:
                  action: send_email
                  inputs:
                    to: client@acme.com
                    subject: Invoice #4521 — $12,400 due
                    body: Hi Sarah, attached is invoice #4521...
                  context:
                    agent_role: AR collections bot
                    user_intent: Send overdue invoice reminder
                    prior_actions: [fetched_invoice_4521, verified_amount_against_stripe]
                  options:
                    depth: standard
              sql:
                summary: SQL write check
                value:
                  action: run_sql
                  inputs:
                    statement: "UPDATE customers SET status = 'archived' WHERE last_login < '2024-01-01'"
                  context:
                    agent_role: data cleanup bot
                    user_intent: Archive long-inactive customers
                  options:
                    depth: deep
      responses:
        "200":
          description: Forecast successfully generated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ForecastResponse"
        "400":
          description: Missing or malformed fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "402":
          description: Insufficient tokens — top up to continue
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before retrying
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/receipts/{id}:
    get:
      summary: Look up a previously-issued decision receipt
      description: |
        Returns the envelope metadata (hashes + Ed25519 signature) for a
        receipt issued by an earlier `/v1/forecast` call. Black_Wall stores
        only the envelope — never the raw request or response bodies — so this
        endpoint returns the cryptographic fingerprint, not the payloads.

        Auth: requires the same Bearer key that issued the receipt.
        Cross-tenant lookups return 404, not 403 (does not leak existence).

        To verify, combine this envelope with your stored copy of the original
        request + response and POST to `/v1/receipts/verify`.
      operationId: getReceipt
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The receipt ID returned in the `/v1/forecast` response under `receipt.id`.
      responses:
        "200":
          description: Receipt envelope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Receipt"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found, or owned by a different tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/receipts/verify:
    post:
      summary: Verify a decision receipt (stateless, public)
      description: |
        Public, no API key required. Verifies an Ed25519 decision receipt
        against the request + response bodies you provide. Intended for
        auditors and any third party who needs to confirm that Black_Wall
        actually signed off on a specific (request, response) pair.

        Stateless: no database lookup. The receipt envelope carries the
        `key_id`; we load the matching published public key from
        `/.well-known/blackwall-signing-keys.json` and verify the signature
        over the canonical hashes of your bodies.

        If you want to verify entirely offline (no network call to Black_Wall):
        cache the public key from the well-known URL and run the same
        Ed25519 verify locally — this endpoint is a convenience, not a trust
        dependency.

        Strip the `receipt` field from your response_body before posting —
        the signature is over the response WITHOUT the envelope. (We tolerate
        it if you forget; we'll strip it for you.)
      operationId: verifyReceipt
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [envelope, request_body, response_body]
              properties:
                envelope:
                  $ref: "#/components/schemas/Receipt"
                request_body:
                  type: object
                  description: The exact body you originally POSTed to `/v1/forecast`.
                  additionalProperties: true
                response_body:
                  type: object
                  description: The exact response body returned by `/v1/forecast`, with the `receipt` field removed.
                  additionalProperties: true
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                type: object
                required: [valid, reason]
                properties:
                  valid:
                    type: boolean
                  reason:
                    type: string
                    description: |
                      ok | request_hash_mismatch | response_hash_mismatch |
                      signature_invalid | verification_error
                    example: ok
        "400":
          description: Missing or malformed fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/forecast/{id}/outcome:
    patch:
      summary: Report the actual outcome of a forecast
      description: |
        After your agent executes (or aborts) the action, send back what
        actually happened. This builds the dataset that compounds prediction
        accuracy for all Black_Wall customers (yours and others). Opt-in.

        Reporting outcomes is FREE — no token cost.
      operationId: reportOutcome
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The forecast ID returned in the original POST response
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OutcomeReport"
      responses:
        "200":
          description: Outcome recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  id:
                    type: string
                    description: The forecast id the outcome was recorded against.
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Forecast not found (or not owned by caller)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/usage:
    get:
      summary: Current token balance and recent usage
      operationId: getUsage
      responses:
        "200":
          description: Usage summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageSummary"

  /v1/keys:
    get:
      summary: List API keys (prefix only)
      operationId: listKeys
      responses:
        "200":
          description: List of keys
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ApiKey"
    post:
      summary: Create a new API key
      operationId: createKey
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "201":
          description: |
            New key created. **The full key is returned ONCE in this response.**
            Store it; it cannot be retrieved again.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiKey"
                  - type: object
                    properties:
                      key:
                        type: string
                        example: bw_live_a1b2c3d4e5f6...

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        Pass your API key as a Bearer token in the Authorization header.
        Keys are prefixed `bw_live_` (or `bw_test_` if you're in test mode).

  schemas:
    ForecastRequest:
      type: object
      required: [action, inputs]
      properties:
        action:
          type: string
          description: |
            What the agent intends to do. Free-form, but consistent values help
            us build per-action accuracy stats. Common values:
            send_email, send_sms, slack_post, run_sql, file_write, file_delete,
            api_call, make_payment, refund, transfer, issue_invoice,
            grant_permission, rotate_key, create_share_link,
            purchase, accept_contract, publish_content.
          example: send_email
        inputs:
          type: object
          description: The concrete payload of the action.
          additionalProperties: true
          example:
            to: client@acme.com
            subject: Invoice #4521 — $12,400 due
            body: Hi Sarah, attached is invoice #4521...
        context:
          type: object
          properties:
            agent_role:
              type: string
              example: AR collections bot for SaaS startup
            user_intent:
              type: string
              example: Send overdue invoice reminder
            prior_actions:
              type: array
              items:
                type: string
              example: [fetched_invoice_4521, verified_amount_against_stripe]
            prior_findings:
              type: array
              description: |
                Optional risk signals from analysis tools that ran BEFORE this call
                (e.g. a multi-agent test harness that flagged a risky agent, tool, or
                interaction edge offline). The gate treats these as PRIORS, not verdicts:
                a finding that matches this action raises the risk and can justify
                CONFIRM/STOP, but the runtime forecast still decides from the concrete
                action + inputs + reversibility. Findings contradicted by the actual
                inputs are down-weighted.
              items:
                type: object
                properties:
                  source:
                    type: string
                    description: What produced the finding.
                    example: swarm-test
                  agent_role:
                    type: string
                    example: data cleanup bot
                  tool_name:
                    type: string
                    example: run_sql
                  edge:
                    type: string
                    description: Interaction edge, source_role → target_role.
                    example: planner → executor
                  risk_type:
                    type: string
                    example: cascade
                  severity:
                    type: string
                    enum: [low, medium, high, critical]
                    example: critical
                  blast_radius:
                    type: integer
                    minimum: 0
                    maximum: 100
                    example: 80
                  recommendation:
                    type: string
                    description: The upstream tool's suggested verdict (advisory only).
                    example: STOP
                  note:
                    type: string
              example:
                - source: swarm-test
                  agent_role: data cleanup bot
                  tool_name: run_sql
                  edge: planner → executor
                  risk_type: cascade
                  severity: critical
                  blast_radius: 80
                  recommendation: STOP
        options:
          type: object
          properties:
            depth:
              type: string
              enum: [standard, deep]
              default: standard
              description: "deep mode costs ~1.5-2x more tokens (and runs ~10-13s vs 4-8s) but returns reasoning trace + counterfactuals + mitigations"

    ForecastResponse:
      type: object
      required: [id, recommendation, risk_score, gate, reversibility, predicted_result, red_flags, tokens_charged, latency_ms]
      properties:
        id:
          type: string
          example: fc_01JKABCD1234567890
        parent_forecast_id:
          type: string
          description: |
            Echoed only when the request supplied parent_forecast_id (per-call swarm
            threading) — links this forecast to the parent action it was run under.
        recommendation:
          type: string
          enum: [GO, CAUTION, STOP]
        risk_score:
          type: integer
          minimum: 0
          maximum: 100
        confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
        gate:
          type: string
          enum: [AUTO, CONFIRM, HUMAN_REQUIRED]
          description: |
            The actionable control, derived from risk_score AND reversibility:
            AUTO = proceed automatically; CONFIRM = hold for human confirmation;
            HUMAN_REQUIRED = block until a human approves. An irreversible action
            is gated at a lower risk threshold than a reversible one.
        reversibility:
          type: object
          description: Can the action be undone, and at what cost?
          properties:
            class:
              type: string
              enum: [REVERSIBLE, RECOVERABLE, IRREVERSIBLE]
            rollback_cost:
              type: integer
              minimum: 0
              maximum: 100
              description: 0 = trivially undoable, 100 = permanent/catastrophic
            rollback_window_sec:
              type: integer
              nullable: true
              description: Seconds available to undo; null = no window / permanent
            rationale:
              type: string
        predicted_result:
          type: object
          properties:
            outcome:
              type: string
              description: One-sentence prediction
              example: Email delivers; likely customer response is confusion or pushback.
            side_effects:
              type: array
              items:
                type: string
        red_flags:
          type: array
          items:
            type: object
            required: [severity, code, message]
            properties:
              severity:
                type: string
                enum: [low, medium, high, critical]
              code:
                type: string
                example: AMOUNT_UNVERIFIED
              message:
                type: string
        alternative_actions:
          type: array
          items:
            type: string
        decision_trace:
          type: array
          items:
            type: string
          description: Only present when depth is "deep"
        counterfactuals:
          type: array
          items:
            type: string
          description: Only present when depth is "deep"
        mitigations:
          type: array
          items:
            type: string
          description: Only present when depth is "deep"
        prior_findings_truncated:
          type: integer
          minimum: 1
          description: |
            Only present when the per-call cap dropped some caller-supplied
            context.prior_findings — either more than 100 findings, or the array's
            total serialized size exceeded the byte ceiling. The value is how many
            findings were dropped (the gate scored a subset of your swarm). Absent
            when nothing was dropped. Send fewer/smaller findings per call to clear it.
        tokens_charged:
          type: integer
          example: 87
        latency_ms:
          type: integer
          example: 3400
        receipt:
          $ref: "#/components/schemas/Receipt"

    Receipt:
      type: object
      description: |
        Cryptographic decision receipt — an Ed25519 signature over canonical
        hashes of the request + response of a single forecast call. Anyone
        with the published public key (see /.well-known/blackwall-signing-keys.json)
        can verify offline without contacting Black_Wall.

        We sign and store HASHES ONLY — the raw request/response bodies are
        never persisted on our side. To verify later, you (or your auditor)
        provide the bodies and we re-hash to check against the signed envelope.
      required: [id, forecast_id, issued_at, algorithm, key_id, request_hash, response_hash, signature]
      properties:
        id:
          type: string
          description: Receipt's own UUID (distinct from forecast_id).
          example: 1b3f25a8-9c4e-4f0a-9b1d-2d0e5c6b7a89
        forecast_id:
          type: string
          description: The forecast this receipt vouches for.
        issued_at:
          type: string
          format: date-time
        algorithm:
          type: string
          enum: [ed25519]
        key_id:
          type: string
          description: |
            Which signing key was used. Look it up in
            /.well-known/blackwall-signing-keys.json. Receipts issued before
            a key rotation continue to verify against the retired key.
          example: k1
        request_hash:
          type: string
          description: SHA-256 (hex) of canonical(request_body), prefixed `sha256:`.
          example: "sha256:9af4...8c2e"
        response_hash:
          type: string
          description: SHA-256 (hex) of canonical(response_body without receipt), prefixed `sha256:`.
          example: "sha256:3d1c...e7b0"
        signature:
          type: string
          description: Base64url Ed25519 signature over the canonicalized envelope (minus signature).
        verify_url:
          type: string
          format: uri
          description: Convenience URL for POST verification.
          example: https://blackwalltier.com/api/v1/receipts/verify

    OutcomeReport:
      type: object
      required: [actual_outcome]
      description: |
        Body for PATCH /v1/forecast/{id}/outcome. Reports what actually
        happened after the action ran (or after it was deliberately not run
        because of a STOP verdict). The `actual_outcome` object is free-form
        jsonb on the server side, but the MCP `observe` tool and Black_Wall's
        admin dashboard recognize the well-known fields below — using them
        gives consistent rendering and (later) accuracy-tracking analytics.
      properties:
        actual_outcome:
          type: object
          properties:
            outcome_class:
              type: string
              enum: [matched, over_scope, under_scope, no_op, diverged, aborted]
              description: |
                How the actual outcome compared to the prediction.
                matched = exactly as predicted.
                over_scope = affected MORE than predicted (e.g. DELETE hit 1247 rows when 1 was expected).
                under_scope = affected less than predicted.
                no_op = action ran but had no effect.
                diverged = result was qualitatively different.
                aborted = action was NOT taken (e.g. agent obeyed a STOP/HUMAN_REQUIRED verdict).
            divergence_severity:
              type: string
              enum: [none, low, medium, high, critical]
              description: How bad the divergence was. Use `none` for `matched` or `aborted`.
            actual_targets:
              type: array
              items:
                type: string
              description: IDs / paths / hashes of what was actually affected.
            details:
              type: string
              description: Free-form details — error messages, observed side effects, etc.
              maxLength: 2000
            matched_prediction:
              type: boolean
              description: |
                Deprecated boolean shortcut, kept for backward compatibility with the original
                outcome shape. Prefer `outcome_class`.
          additionalProperties: true
        customer_notes:
          type: string

    UsageSummary:
      type: object
      properties:
        plan:
          type: string
          enum: [free, starter, scale, enterprise]
        token_balance:
          type: integer
        monthly_grant:
          type: integer
        last_30_days_calls:
          type: integer
        last_30_days_tokens_spent:
          type: integer

    ApiKey:
      type: object
      properties:
        id:
          type: string
        prefix:
          type: string
          example: bw_live_
        name:
          type: string
          nullable: true
        last_used_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    Error:
      type: object
      properties:
        error:
          type: string
          description: Machine-readable error slug
          example: insufficient_tokens
        message:
          type: string
          description: Human-readable explanation
        details:
          type: object
          additionalProperties: true
