{
  "openapi": "3.0.3",
  "info": {
    "title": "Operon x402 Advertiser API",
    "description": "Self-serve advertiser onboarding for the Operon ad network. Three HTTP routes run the full advertiser lifecycle: create a funded campaign via x402, read campaign state, and cancel with refund of unspent balance. Live on Base mainnet.",
    "version": "1.0.0",
    "contact": {
      "name": "Operon",
      "email": "hi@operon.so",
      "url": "https://operon.so/x402"
    },
    "termsOfService": "https://operon.so/x402#pricing",
    "license": {
      "name": "Apache-2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0"
    }
  },
  "servers": [
    {
      "url": "https://api.operon.so",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "campaign",
      "description": "Advertiser campaign lifecycle"
    }
  ],
  "paths": {
    "/x402/campaign": {
      "post": {
        "operationId": "createCampaign",
        "security": [],
        "tags": ["campaign"],
        "summary": "Create and fund an advertiser campaign",
        "description": "x402-gated. The first call (no `X-PAYMENT` header) returns HTTP 402 with a JSON body containing the payment challenge per the x402 protocol. Construct a `PaymentPayload` from the challenge's `accepts[0]`, base64-encode the JSON, send it as the `X-PAYMENT` request header, and retry. On verification + on-chain settlement, the server creates the campaign, fires a fire-and-forget trust-index registration for the endpoint domain, and returns 201 with the bearer token (returned exactly once). The campaign enters Operon's quality-weighted auction immediately. Trust score is null at creation and populates as behavioral signal builds.",
        "parameters": [
          {
            "name": "X-PAYMENT",
            "in": "header",
            "required": false,
            "schema": { "type": "string" },
            "description": "Base64-encoded JSON `PaymentPayload` constructed against the challenge returned by the 402 response. Absent on the initial request, present on the retry."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campaign created and funded. The `X-PAYMENT-RESPONSE` header carries the base64-encoded settlement receipt.",
            "headers": {
              "X-PAYMENT-RESPONSE": {
                "schema": { "type": "string" },
                "description": "Base64-encoded JSON. Settlement receipt: `{success, transaction, network, payer}`."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignCreateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (e.g. service_url unreachable, unknown category, missing required field, malformed X-PAYMENT payload).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "402": {
            "description": "Payment required, or payment verification/settlement failed. Body is the x402 challenge. Construct a `PaymentPayload` against `accepts[0]`, base64-encode, set as the `X-PAYMENT` request header, and retry. `error` is `null` on the initial challenge and populated with a reason string when a submitted payment fails (e.g. `payment verification failed`, `payment settlement failed`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/X402PaymentChallenge" }
              }
            }
          },
          "413": {
            "description": "Request body exceeded the x402 body size cap.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "Per-IP campaign-create rate limit (10/min) exceeded.",
            "headers": {
              "Retry-After": { "schema": { "type": "string" } }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "503": {
            "description": "x402 onboarding not enabled in this environment.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/x402/campaign/{id}": {
      "get": {
        "operationId": "readCampaign",
        "tags": ["campaign"],
        "summary": "Read campaign state",
        "description": "Returns current balance, stats, status, and trust score. No payment required. Bearer auth using the token issued at campaign creation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Campaign ID",
            "schema": { "type": "string" }
          }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Current campaign state",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CampaignReadResponse" }
              }
            }
          },
          "401": {
            "description": "Campaign not found or invalid bearer token. The server intentionally unifies these two cases into a single 401 to prevent campaign-id enumeration. No 404 is returned for missing campaigns.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "Per-IP read rate limit (60/min) exceeded.",
            "headers": {
              "Retry-After": { "schema": { "type": "string" } }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "503": {
            "description": "x402 onboarding not enabled in this environment.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "cancelCampaign",
        "tags": ["campaign"],
        "summary": "Cancel campaign and queue refund",
        "description": "Cancels the campaign and queues a refund of unspent balance to the original x402 payer wallet. Manual processing within 7 days. Spent balance is not refundable. Bearer auth using the token issued at campaign creation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Campaign ID",
            "schema": { "type": "string" }
          }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Cancelled, refund queued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CampaignCancelResponse" }
              }
            }
          },
          "401": {
            "description": "Campaign not found or invalid bearer token. The server intentionally unifies these two cases into a single 401 to prevent campaign-id enumeration. No 404 is returned for missing campaigns.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "Per-IP cancel rate limit (60/min) exceeded.",
            "headers": {
              "Retry-After": { "schema": { "type": "string" } }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "503": {
            "description": "x402 onboarding not enabled in this environment.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer token issued at campaign creation. Used for GET and DELETE on the same campaign."
      }
    },
    "schemas": {
      "Category": {
        "type": "string",
        "description": "Targeting category. Matches the publisher SDK targeting taxonomy.",
        "enum": [
          "defi",
          "fintech",
          "travel",
          "insurance",
          "ecommerce",
          "saas",
          "health",
          "education",
          "gambling",
          "general"
        ]
      },
      "Intent": {
        "type": "string",
        "maxLength": 64,
        "description": "Optional intent qualifier. Free-form string; the server accepts any non-empty value up to 64 chars and does not enforce a fixed taxonomy. Common values surfaced by the publisher SDK: `research`, `comparison`, `recommendation`, `transaction`. Other values (e.g. `swap`, `bridge`) are valid."
      },
      "CampaignStatus": {
        "type": "string",
        "enum": ["active", "paused", "depleted", "cancelled"]
      },
      "CampaignCreateRequest": {
        "type": "object",
        "required": ["service_url", "service_name", "category"],
        "properties": {
          "service_url": {
            "type": "string",
            "format": "uri",
            "description": "Where the advertiser's service lives. Must be reachable - the server HEAD-pings before accepting the campaign. 200/30x = pass."
          },
          "service_name": {
            "type": "string",
            "maxLength": 64,
            "description": "Display name shown in placements"
          },
          "category": { "$ref": "#/components/schemas/Category" },
          "asset": {
            "type": "string",
            "description": "Optional. Free-form, narrows targeting (e.g. 'ETH', 'yield-farming')."
          },
          "intent": { "$ref": "#/components/schemas/Intent" },
          "bid_per_placement": {
            "type": "number",
            "format": "float",
            "minimum": 0,
            "exclusiveMinimum": true,
            "maximum": 100,
            "description": "Optional. USDC. Must be greater than zero and at most 100 USDC. Defaults to mid-range CPM for the category if omitted."
          },
          "daily_cap": {
            "type": "number",
            "format": "float",
            "minimum": 0,
            "exclusiveMinimum": true,
            "description": "Optional. USDC. Must be greater than zero. Spreads balance across days; campaign auto-pauses at end-of-day until next-day reset."
          }
        }
      },
      "CampaignCreateResponse": {
        "type": "object",
        "required": [
          "campaign_id",
          "balance_usdc",
          "category",
          "bid_per_placement",
          "status",
          "created_at",
          "x402_payer_wallet",
          "bearer_token"
        ],
        "properties": {
          "campaign_id": { "type": "string" },
          "balance_usdc": {
            "type": "number",
            "format": "float",
            "description": "Funded balance in USDC. Starts at 100 (the minimum deposit)."
          },
          "category": { "$ref": "#/components/schemas/Category" },
          "asset": { "type": "string", "nullable": true },
          "intent": {
            "type": "string",
            "nullable": true,
            "maxLength": 64,
            "description": "Optional intent qualifier. Free-form string; the server does not enforce a fixed taxonomy. Common values: `research`, `comparison`, `recommendation`, `transaction`. Null when unset."
          },
          "bid_per_placement": {
            "type": "number",
            "format": "float",
            "description": "Resolved bid (input value or category default)."
          },
          "daily_cap": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "trust_score": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "0-100 trust score. Null at creation; populates as Operon's trust monitoring gathers behavioral signal on the endpoint."
          },
          "status": { "$ref": "#/components/schemas/CampaignStatus" },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "x402_payer_wallet": {
            "type": "string",
            "description": "Redacted wallet address (first 6 / last 4 of the x402 payer). Refunds route to the full wallet stored server-side."
          },
          "bearer_token": {
            "type": "string",
            "description": "Bearer token for subsequent GET and DELETE on this campaign id. Returned EXACTLY ONCE on creation; never on reads. Persist immediately - it cannot be recovered."
          }
        }
      },
      "CampaignReadResponse": {
        "type": "object",
        "required": [
          "campaign_id",
          "balance_usdc",
          "balance_spent_usdc",
          "category",
          "bid_per_placement",
          "status",
          "stats",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "campaign_id": { "type": "string" },
          "balance_usdc": {
            "type": "number",
            "format": "float",
            "description": "Remaining USDC in funded balance."
          },
          "balance_spent_usdc": {
            "type": "number",
            "format": "float",
            "description": "Cumulative spent USDC."
          },
          "category": { "$ref": "#/components/schemas/Category" },
          "asset": { "type": "string", "nullable": true },
          "intent": {
            "type": "string",
            "nullable": true,
            "maxLength": 64,
            "description": "Optional intent qualifier. Free-form string; the server does not enforce a fixed taxonomy. Common values: `research`, `comparison`, `recommendation`, `transaction`. Null when unset."
          },
          "bid_per_placement": { "type": "number", "format": "float" },
          "daily_cap": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "trust_score": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "0-100 trust score. Null until behavioral signal builds."
          },
          "status": { "$ref": "#/components/schemas/CampaignStatus" },
          "stats": {
            "type": "object",
            "required": [
              "impressions_served",
              "wins",
              "click_throughs",
              "average_clearing_price_usdc"
            ],
            "properties": {
              "impressions_served": { "type": "integer", "minimum": 0 },
              "wins": { "type": "integer", "minimum": 0 },
              "click_throughs": { "type": "integer", "minimum": 0 },
              "average_clearing_price_usdc": {
                "type": "number",
                "format": "float",
                "minimum": 0
              }
            }
          },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "CampaignCancelResponse": {
        "type": "object",
        "required": [
          "campaign_id",
          "status",
          "refund_amount_usdc",
          "refund_destination_wallet",
          "refund_status",
          "refund_eta"
        ],
        "properties": {
          "campaign_id": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["cancelled"]
          },
          "refund_amount_usdc": {
            "type": "number",
            "format": "float",
            "description": "Equal to balance_usdc at the time of cancellation. Spent balance is not refundable. Zero on idempotent re-cancel."
          },
          "refund_destination_wallet": {
            "type": "string",
            "description": "Equal to x402_payer_wallet captured at campaign creation."
          },
          "refund_status": {
            "type": "string",
            "enum": ["queued", "already_processed"],
            "description": "`queued` on first cancel. `already_processed` on idempotent re-cancel of an already-cancelled campaign; in that case `refund_amount_usdc` is 0 and `refund_eta` is null."
          },
          "refund_eta": {
            "type": "string",
            "nullable": true,
            "description": "Refund processing window. v1 is manual within 7 days. Null when `refund_status` is `already_processed`.",
            "example": "within 7 days"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error string. Specific enough to action without parsing."
          }
        }
      },
      "X402PaymentChallenge": {
        "type": "object",
        "description": "402 response body per the x402 protocol. Sent on the initial unauthenticated call and again on any verify/settle failure.",
        "required": ["x402Version", "accepts", "error"],
        "properties": {
          "x402Version": {
            "type": "integer",
            "enum": [1],
            "description": "x402 protocol version."
          },
          "accepts": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/PaymentRequirements" },
            "description": "List of acceptable payment requirements. Construct a PaymentPayload against the first entry and retry with `X-PAYMENT` set."
          },
          "error": {
            "type": "string",
            "nullable": true,
            "description": "Null on the initial challenge. Populated with a reason string when a submitted X-PAYMENT failed verification or settlement."
          }
        }
      },
      "PaymentRequirements": {
        "type": "object",
        "description": "x402 protocol payment requirement. Carries the network, asset, amount, and pay-to wallet required to satisfy the challenge. Shape is defined by the x402 protocol (https://x402.org) and may evolve independently of this spec; clients should treat unknown fields permissively.",
        "additionalProperties": true
      }
    }
  }
}
