openapi: 3.1.0
info:
  title: InsumerAPI
  version: "1.0"
  description: >
    InsumerAPI is the API for wallet auth — the primitive: read on-chain state, evaluate against caller-supplied conditions, return ECDSA-signed booleans across 37 chains. Conditions in, signed attestations out. Provable and private — no shared secrets, no identity-first claims, no static credentials. Never exposes wallet balances or requires trust in the API provider.
    InsumerAPI is the programmatic interface for The Insumer Model — used by developers, AI agents, and platforms alike. One endpoint checks token balances, NFT ownership, and EAS attestations across 37 blockchains, returns ECDSA P-256 signed booleans, and never exposes wallet balances.
    Supports up to 10 conditions per request, each with its own chainId. Optional Merkle storage proofs (EIP-1186) for trustless verification.
    Pre-configured compliance templates for Coinbase Verifications (KYC, country, Coinbase One) on Base.
    Not a loyalty program. Not a reputation network. Not an identity system. Not a DeFi protocol. Not a payments processor. Not an oracle network.
    Used by AsterPay KYA (ERC-8183 agent trust scoring), Revettr (counterparty risk for x402), and SettlementWitness (pre/post transaction verification).
    4 agent SDKs: MCP server (npx -y mcp-server-insumer), LangChain (pip install langchain-insumer), ElizaOS (@insumermodel/plugin-eliza), OpenAI GPT (GPT Store).
    2 framework adapters: WDK protocol module (@insumermodel/wdk-protocol-wallet-auth) for Tether's Wallet Development Kit; mppx condition-gate (@insumermodel/mppx-condition-gate) for Machine Payments Protocol routes (first listed entry on Tempo's extensions page).
    Attestations independently verifiable with insumer-verify (npm install insumer-verify).
    Full guide: https://insumermodel.com/ai-agent-verification-api/
    Visual endpoint map: https://insumermodel.com/workbench/
  contact:
    email: contact@insumermodel.com
    url: https://insumermodel.com/developers/
  termsOfService: https://insumermodel.com/terms-of-service/
  license:
    name: Proprietary

servers:
  - url: https://api.insumermodel.com
    description: Production
  # Legacy endpoint removed — use api.insumermodel.com exclusively

security:
  - ApiKeyAuth: []

x-auth-note: "Default authentication is the X-API-Key header. Five endpoints — POST /v1/attest, POST /v1/trust, POST /v1/trust/batch, GET /v1/credits, and POST /v1/credits/buy — also accept wallet-signed auth via an Authorization: Wallet header (SIWE envelope) from a wallet that holds an Insumer Access pass. All other endpoints require X-API-Key. A 401 response with {ok: false, error: {code: 401, message: 'Invalid or missing API key.'}} is returned for any request with no valid credential."

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key in format: insr_live_ followed by 40 hex characters"
    WalletAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        Wallet-signed authentication. Accepted on: POST /v1/attest, POST /v1/trust,
        POST /v1/trust/batch, GET /v1/credits, and POST /v1/credits/buy. Other
        endpoints return 501 for this scheme.
        Header value: `Wallet <base64(JSON({message, signature}))>` where
        `message` is a SIWE (EIP-4361) string with `Domain: api.insumermodel.com`
        and `signature` is an EIP-191 personal_sign over `message`. The signing
        wallet must (1) match the wallet registered to the API key (the sender
        wallet from POST /v1/keys/buy) and (2) hold the Insumer Access pass
        (soulbound ERC-721 on Base, collection 0x3E2a408cc6eceba04FF9d04A5B8B05aBa8DD50ce).
        Nonce is single-use; `Issued At` must be within the last 5 minutes.
        Drop-in middleware available as `@skyemeta/access` on npm.

  schemas:
    ChainId:
      oneOf:
        - type: integer
          enum: [1, 56, 8453, 43114, 137, 42161, 10, 88888, 1868, 98866, 146, 100, 5000, 534352, 59144, 324, 81457, 167000, 2020, 42220, 1284, 1285, 88, 204, 480, 130, 57073, 1329, 80094, 33139, 50]
        - type: string
          enum: ["solana", "xrpl", "bitcoin", "tron", "stellar", "sui"]
      description: >
        EVM chain ID (integer), or one of "solana", "xrpl", "bitcoin", "tron",
        "stellar", "sui" (string).
        Supported chains: Ethereum (1), BNB Chain (56), Base (8453), Avalanche (43114),
        Polygon (137), Arbitrum (42161), Optimism (10), Chiliz (88888), Soneium (1868),
        Plume (98866), Sonic (146), Gnosis (100), Mantle (5000), Scroll (534352),
        Linea (59144), zkSync Era (324), Blast (81457), Taiko (167000), Ronin (2020),
        Celo (42220), Moonbeam (1284), Moonriver (1285), Viction (88), opBNB (204),
        World Chain (480), Unichain (130), Ink (57073), Sei (1329), Berachain (80094),
        ApeChain (33139), XDC Network (50), Solana ("solana"), XRP Ledger ("xrpl"),
        Bitcoin ("bitcoin"), Tron ("tron"), Stellar ("stellar"), Sui ("sui").

    OnboardingChainId:
      oneOf:
        - type: integer
          enum: [1, 56, 8453, 43114, 137, 42161, 10, 88888, 1868, 98866, 480, 146, 100, 5000, 534352, 59144, 324, 81457, 42220, 1284, 204, 130, 57073, 1329, 80094, 33139, 50]
        - type: string
          enum: ["solana", "xrpl", "bitcoin", "tron", "stellar", "sui"]
      description: >
        Chains supported for merchant token/NFT configuration (27 EVM chains + Solana + XRPL + Bitcoin + Tron + Stellar + Sui).
        Excludes Taiko, Ronin, Moonriver, and Viction from merchant registry, though all four remain queryable via /v1/attest and /v1/trust.
        Bitcoin only supports token_balance with contractAddress "native". Tron, Stellar, and Sui support token_balance only.

    PaymentChainId:
      oneOf:
        - type: integer
          enum: [1, 8453, 137, 42161, 10, 56, 43114]
        - type: string
          enum: ["solana", "bitcoin", "tron"]
      description: "Chains supported for crypto payments. EVM chains and Solana accept USDC and USDT. Bitcoin accepts BTC (converted to USD at market rate, requires 1 confirmation). Tron accepts USDT-TRC20."

    # Backwards-compatible alias
    UsdcChainId:
      $ref: "#/components/schemas/PaymentChainId"

    SuccessEnvelope:
      type: object
      required: [ok, data, meta]
      properties:
        ok:
          type: boolean
          const: true
        data:
          type: object
        meta:
          $ref: "#/components/schemas/Meta"

    ErrorEnvelope:
      type: object
      required: [ok, error, meta]
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: integer
            message:
              type: string
        meta:
          $ref: "#/components/schemas/Meta"

    RpcFailureEnvelope:
      type: object
      description: >
        Returned when one or more upstream data sources are
        unavailable after retries. No attestation is signed, no JWT issued, and no credits
        are charged. This is a retryable error — the caller should retry the same request
        after a short delay (e.g. 2-5 seconds). Do NOT treat this as a permanent verification
        failure.
      required: [ok, error, meta]
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: ["rpc_failure"]
            message:
              type: string
              example: "Unable to verify all conditions — data source unavailable after retries"
            failedConditions:
              type: array
              description: "Which data fetches failed. Useful for debugging which chain is down."
              items:
                type: object
                properties:
                  chainId:
                    type: string
                    description: "Chain ID that failed"
                  message:
                    type: string
                    description: "Error message from the failed fetch"
        meta:
          $ref: "#/components/schemas/Meta"

    Meta:
      type: object
      properties:
        version:
          type: string
          example: "1.0"
        timestamp:
          type: string
          format: date-time

    Condition:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [token_balance, nft_ownership, eas_attestation, farcaster_id, ratio_to_amount, ratio_to_supply]
        contractAddress:
          type: string
          description: "Token or NFT contract address (required for token_balance, nft_ownership, ratio_to_amount, and ratio_to_supply; use \"native\" for the chain's native asset, except ratio_to_supply which requires an ERC-20 contract). For XRPL: use \"native\" for XRP, or the issuer r-address for trust line tokens. For Bitcoin: must be \"native\"."
        chainId:
          $ref: "#/components/schemas/ChainId"
        threshold:
          oneOf:
            - type: number
            - type: string
          description: "Minimum balance required (token_balance type), in token/display units. v1 keys take a JSON number (e.g. 100). v2 keys — which is every newly created key — require a decimal STRING (e.g. \"100\"); a JSON number is rejected with 400. v2 carries the threshold as an exact canonical decimal so an agent can specify full-precision thresholds that a JSON float would silently round. Must be > 0 (use \"0.000001\" / 0.000001 for prove-any-balance)."
        multiple:
          oneOf:
            - type: number
            - type: string
          description: "Required for ratio_to_amount. The collateralization multiple — the condition is met iff balance >= multiple * amount (e.g. 10 for \"hold >= 10x the transaction amount\"). v1 keys take a JSON number; v2 keys — every newly created key — require a decimal STRING (e.g. \"10\") and reject a JSON number with 400. Must be > 0."
        amount:
          oneOf:
            - type: number
            - type: string
          description: "Required for ratio_to_amount. The per-request reference amount (e.g. the transaction size the agent intends to make), in token/display units — e.g. 100 for 100 USDC, NOT base units/wei. v1: JSON number; v2: decimal STRING (e.g. \"100\"). Must be > 0. ratio_to_amount is supported on RPC EVM chains only."
        minFraction:
          oneOf:
            - type: number
            - type: string
          description: "Required for ratio_to_supply. The required share of the token's on-chain total supply, as a fraction in (0, 1] — e.g. 0.005 for 0.5% of supply. The condition is met iff balance / totalSupply() >= minFraction. v1: JSON number; v2: decimal STRING (e.g. \"0.005\"). Use for project/governance tokens where share-of-supply is meaningful (not stablecoins). Supported on RPC EVM chains, ERC-20 contracts only."
        decimals:
          type: integer
          minimum: 0
          maximum: 77
          description: "Token decimals. Auto-detected from the contract if omitted."
        currency:
          type: string
          description: "XRPL currency code (required for XRPL trust line tokens, ignored for other chains). 3-char standard code (e.g. \"USD\") or token name (e.g. \"RLUSD\", auto hex-encoded to 40 chars)."
          example: "RLUSD"
        taxon:
          type: integer
          description: "XRPL NFToken taxon filter (optional, for nft_ownership on XRPL only). Filters NFTs by issuer + taxon."
        label:
          type: string
          maxLength: 100
          description: "Human-readable label for this condition"
        schemaId:
          type: string
          description: "EAS schema ID (bytes32 hex string). Required for eas_attestation unless template is provided."
          example: "0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9"
        attester:
          type: string
          description: "Expected attester address (optional, for eas_attestation). If provided, the attestation's attester must match this address (case-insensitive). Recipient is not a request-side field — it auto-binds to the wallet being verified, and the attestation's recipient field must equal that wallet for the condition to pass."
        indexer:
          type: string
          description: "EAS indexer contract address. **Required** for raw (non-template) eas_attestation conditions. The verifier resolves the attestation UID by calling `getAttestationUid(address recipient, bytes32 schema) returns (bytes32)` (selector 0xab2717dd) on this contract, then fetches and verifies the resolved attestation against the EAS contract on the target chain. Custom EAS issuers must deploy or co-list in such an indexer; see /developers/compliance/#custom-eas-issuers."
        template:
          type: string
          enum: [coinbase_verified_account, coinbase_verified_country, coinbase_one, gitcoin_passport_score, gitcoin_passport_active]
          description: "Pre-configured compliance template name. Use instead of raw schemaId/attester/indexer — the template fills in all three. See GET /v1/compliance/templates for available templates."

    EvaluatedCondition:
      type: object
      description: "The exact condition logic that was evaluated, included for tamper-evidence. Callers can recompute conditionHash from this object to verify integrity."
      properties:
        type:
          type: string
          enum: [token_balance, nft_ownership, eas_attestation, farcaster_id, ratio_to_amount, ratio_to_supply]
        chainId:
          $ref: "#/components/schemas/ChainId"
        contractAddress:
          type: string
          description: "Contract address that was queried (token_balance, nft_ownership, ratio_to_amount, and ratio_to_supply). For XRPL: \"native\" or issuer r-address."
        operator:
          type: string
          enum: [gte, gt, valid, decoder, registered, gte_fraction]
          description: "Comparison operator used: gte (>=) for token_balance and ratio_to_amount, gte_fraction (balance/totalSupply >=) for ratio_to_supply, gt (>) for nft_ownership, valid for eas_attestation, decoder for Gitcoin Passport, registered for farcaster_id"
        threshold:
          oneOf:
            - type: number
            - type: string
          description: "Threshold value used in comparison (human-readable units for token_balance, 0 for nft_ownership). For ratio_to_amount this is the authoritative derived value (= multiple * amount) that balance was compared against. Not present for eas_attestation or ratio_to_supply. v1 keys return a JSON number; v2 keys — every newly created key — return a canonical decimal string (nft_ownership stays the number 0 on both)."
        multiple:
          oneOf:
            - type: number
            - type: string
          description: "Collateralization multiple (present only for ratio_to_amount). Echoed for provenance; threshold (= multiple * amount) is the authoritative comparison value. v1: JSON number; v2: canonical decimal string."
        amount:
          oneOf:
            - type: number
            - type: string
          description: "Reference amount in token/display units (present only for ratio_to_amount). Echoed for provenance; the operative comparison is balance >= threshold. v1: JSON number; v2: canonical decimal string."
        minFraction:
          oneOf:
            - type: number
            - type: string
          description: "Required share of total supply as a fraction in (0, 1] (present only for ratio_to_supply). The condition (balance / totalSupply >= minFraction) is fully described by this plus contractAddress/chainId; the totalSupply read and computed fraction are returned on the result object (siblings of evaluatedCondition), not here, so conditionHash stays a pure rule fingerprint. v1: JSON number; v2: canonical decimal string."
        decimals:
          type: integer
          nullable: true
          description: "Token decimals (present only for token_balance and ratio_to_amount conditions). May be null for Solana and XRPL conditions in attest when not provided in the request. Auto-detected for all 31 EVM chains. Trust profiles always return a non-null value. v2 attest omits this field entirely — the canonical decimal threshold is self-describing."
        currency:
          type: string
          description: "Normalized XRPL currency code (40-char hex for non-standard currencies). Present only for XRPL trust line conditions."
        taxon:
          type: integer
          description: "XRPL NFToken taxon filter. Present only for XRPL nft_ownership conditions when taxon was specified."
        schemaId:
          type: string
          description: "EAS schema ID (present only for eas_attestation conditions)"
        attester:
          type: string
          description: "Expected attester address (present only for eas_attestation conditions when attester was specified)"
        decoder:
          type: string
          description: "Decoder contract address (present only for decoder-based eas_attestation conditions like Gitcoin Passport)"

    AttestationResult:
      type: object
      properties:
        condition:
          type: integer
          description: "Index of the condition in the request array"
        label:
          type: string
        type:
          type: string
          enum: [token_balance, nft_ownership, eas_attestation, farcaster_id, ratio_to_amount, ratio_to_supply]
        chainId:
          $ref: "#/components/schemas/ChainId"
        met:
          type: boolean
        evaluatedCondition:
          $ref: "#/components/schemas/EvaluatedCondition"
        conditionHash:
          type: string
          description: "SHA-256 hash of the canonical (sorted-key) JSON of evaluatedCondition, prefixed with 0x. Callers can recompute this to verify the condition was not tampered with."
          example: "0x3a7f1b2c..."
        totalSupply:
          type: string
          description: "Raw on-chain totalSupply() read at blockNumber (integer as a string), present only for ratio_to_supply. A reproducibility reference: re-read totalSupply at blockNumber to confirm. Inside the signed results payload but outside conditionHash."
          example: "52448506631611846"
        fraction:
          type: number
          description: "Computed balance / totalSupply at blockNumber (present only for ratio_to_supply). Display/transparency value; verifiers should re-derive met from the raw on-chain reads rather than this float. Can exceed 1.0 for pathological tokens where balanceOf > totalSupply."
          example: 5.93479e-10
        blockNumber:
          type: string
          description: "Hex block number at which the condition was evaluated. Present on all 31 EVM chains. Solana conditions return slot instead; XRPL conditions return ledgerIndex. The API refuses to sign any attestation whose block anchor could not be captured, returning 503 instead."
          example: "0x12f4a80"
        blockTimestamp:
          type: string
          format: date-time
          description: "ISO 8601 timestamp of the block at which the condition was evaluated. Present on all 31 EVM chains and co-required with blockNumber."
          example: "2026-02-26T12:34:56.000Z"
        ledgerIndex:
          type: integer
          description: "XRPL validated ledger index at which the condition was evaluated. Present only for XRPL conditions."
          example: 102575456
        ledgerHash:
          type: string
          description: "XRPL validated ledger hash for the ledger at which the condition was evaluated. Present only for XRPL conditions."
          example: "BB9023D447285923C36E3DF18EF0FC37A5A6A1113527FB5F6FCE3139AAD72388"
        trustLineState:
          type: object
          description: "Trust line state flags for XRPL trust line token conditions. Present only for non-native XRPL token_balance conditions."
          properties:
            frozen:
              type: boolean
              description: "Whether the trust line is frozen (issuer or holder freeze). A frozen line means the balance is not spendable."
        slot:
          type: integer
          description: "Solana slot at which the condition was evaluated. Present only for Solana conditions. Verifiers can reproduce the read by querying wallet state at this slot or later."
          example: 338472915
        blockHeight:
          type: integer
          description: "Bitcoin chain tip block height at the time of wallet state observation. Present only for Bitcoin conditions. Evidence-of-freshness anchor — Bitcoin's UTXO model does not permit reproducible balance-at-block queries, so this height binds the observation to a recent tip rather than to a specific block's state."
          example: 946083
        blockHash:
          type: string
          description: "Bitcoin chain tip block hash (64-character lowercase hex) at the time of wallet state observation. Present only for Bitcoin conditions. Co-reported with blockHeight."
          example: "000000000000000000003c5ff410ed3c66b3cb803c2aac90b5b6600b20ba91e5"
        proof:
          description: "Present only when proof: \"merkle\" is requested"
          oneOf:
            - $ref: "#/components/schemas/MerkleProof"
            - $ref: "#/components/schemas/MerkleProofUnavailable"

    MerkleProof:
      type: object
      description: "EIP-1186 Merkle storage proof for trustless verification against block headers"
      properties:
        available:
          type: boolean
          const: true
        type:
          type: string
          const: "merkle"
        blockNumber:
          type: string
          description: "Hex block number at which the proof was generated"
          example: "0x12a05f200"
        mappingSlot:
          type: integer
          description: "ERC-20 balanceOf mapping slot in contract storage, resolved automatically by the API"
        storageKey:
          type: string
          description: "keccak256(abi.encode(address, uint256(slot))) storage key"
        accountProof:
          type: array
          items:
            type: string
          description: "Merkle proof nodes from state root to account"
        storageProof:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              value:
                type: string
                description: "Raw token balance (hex, before decimal division)"
              proof:
                type: array
                items:
                  type: string
          description: "Merkle proof nodes from storage root to balance slot"
        storageHash:
          type: string
          description: "Storage root hash of the contract account"

    MerkleProofUnavailable:
      type: object
      description: "Returned when Merkle proof is not available for a condition"
      properties:
        type:
          type: string
          const: "merkle"
        available:
          type: boolean
          const: false
        reason:
          type: string
          description: "Why the proof is unavailable (e.g. unsupported chain, Solana, nft_ownership, zero balance)"

    TrustCheck:
      type: object
      properties:
        label:
          type: string
          description: "Human-readable label (e.g. 'USDC on Ethereum')"
        chainId:
          $ref: "#/components/schemas/ChainId"
        met:
          type: boolean
          description: "Whether the wallet has a non-zero balance"
        evaluatedCondition:
          $ref: "#/components/schemas/EvaluatedCondition"
        conditionHash:
          type: string
          description: "SHA-256 of canonical evaluatedCondition JSON, 0x-prefixed"
        blockNumber:
          type: string
          description: "Hex block number at which the check was evaluated. Present on all 31 EVM chains; Solana checks use slot, XRPL checks use ledgerIndex."
        blockTimestamp:
          type: string
          format: date-time
          description: "ISO 8601 timestamp of the block at which the check was evaluated. Present on all 31 EVM chains."
        ledgerIndex:
          type: integer
          description: "XRPL validated ledger index (XRPL only)"
        ledgerHash:
          type: string
          description: "XRPL validated ledger hash (XRPL only)"
        trustLineState:
          type: object
          description: "Trust line state flags (XRPL trust line tokens only). Frozen lines fail attestation."
          properties:
            frozen:
              type: boolean
        slot:
          type: integer
          description: "Solana slot at which the condition was evaluated. Present only for Solana conditions. Verifiers can reproduce the read by querying wallet state at this slot or later."
          example: 338472915
        blockHeight:
          type: integer
          description: "Bitcoin chain tip block height at the time of wallet state observation. Present only for Bitcoin checks. Evidence-of-freshness anchor — Bitcoin's UTXO model does not permit reproducible balance-at-block queries, so this height binds the observation to a recent tip rather than to a specific block's state."
          example: 946083
        blockHash:
          type: string
          description: "Bitcoin chain tip block hash (64-character lowercase hex) at the time of wallet state observation. Present only for Bitcoin checks. Co-reported with blockHeight."
          example: "000000000000000000003c5ff410ed3c66b3cb803c2aac90b5b6600b20ba91e5"
        proof:
          description: "EIP-1186 Merkle storage proof anchored to the block header, enabling trustless verification without re-querying the chain. Present only when proof: 'merkle' is requested."
          oneOf:
            - $ref: "#/components/schemas/MerkleProof"
            - $ref: "#/components/schemas/MerkleProofUnavailable"

    TrustDimension:
      type: object
      properties:
        checks:
          type: array
          items:
            $ref: "#/components/schemas/TrustCheck"
        passCount:
          type: integer
          description: "Number of checks where met is true"
        failCount:
          type: integer
          description: "Number of checks where met is false"
        total:
          type: integer
          description: "Total checks in this dimension"

    Attestation:
      type: object
      properties:
        id:
          type: string
          description: "Format: ATST- followed by 16 uppercase hex characters"
          example: "ATST-A7C3E1B2D4F56789"
        pass:
          type: boolean
          description: "true only when ALL conditions are met"
        results:
          type: array
          items:
            $ref: "#/components/schemas/AttestationResult"
        passCount:
          type: integer
        failCount:
          type: integer
        attestedAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          description: "Attestation expires after 30 minutes"

    Tier:
      type: object
      required: [name, threshold, discount]
      properties:
        name:
          type: string
          maxLength: 30
        threshold:
          type: number
          exclusiveMinimum: 0
        discount:
          type: integer
          minimum: 1
          maximum: 50

    TokenConfig:
      type: object
      required: [symbol, chainId, contractAddress, tiers]
      properties:
        symbol:
          type: string
          maxLength: 10
        name:
          type: string
          maxLength: 100
          description: "Optional display name shown in the public directory listing (without it the entry shows a bare symbol)."
        logo:
          type: string
          maxLength: 500
          description: "Optional logo URL shown in the public directory listing."
        chainId:
          $ref: "#/components/schemas/OnboardingChainId"
        contractAddress:
          type: string
        decimals:
          type: integer
          minimum: 0
          maximum: 18
          default: 18
        currency:
          type: string
          description: "XRPL trust line currency code (e.g. \"USD\", \"RLUSD\"). Required for XRPL trust line tokens."
        tiers:
          type: array
          items:
            $ref: "#/components/schemas/Tier"
          minItems: 1
          maxItems: 4

    NftConfig:
      type: object
      required: [name, contractAddress, chainId, discount]
      properties:
        name:
          type: string
          maxLength: 50
        contractAddress:
          type: string
        chainId:
          $ref: "#/components/schemas/OnboardingChainId"
        taxon:
          type: integer
          description: "XRPL NFToken taxon filter. Optional, for filtering NFTs by collection under the same issuer."
        discount:
          type: integer
          minimum: 1
          maximum: 50

    MerchantSummary:
      type: object
      properties:
        id:
          type: string
        companyName:
          type: string
        website:
          type: string
        location:
          type: string
        verified:
          type: boolean
        tokens:
          type: array
          items:
            type: object
            properties:
              symbol:
                type: string
              maxDiscount:
                type: integer
        nftCollections:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              discount:
                type: integer
        discountMode:
          type: string
          enum: [highest, stack, capped]
        discountCap:
          type: integer

    MerchantDetail:
      type: object
      properties:
        id:
          type: string
        companyName:
          type: string
        website:
          type: string
        location:
          type: string
        verified:
          type: boolean
        verifiedDomain:
          type: string
        discountMode:
          type: string
          enum: [highest, stack, capped]
          description: "Effective discount mode. 'capped' is returned when discountMode is 'stack' and discountCap limits the total."
        discountCap:
          type: integer
        tokens:
          type: array
          items:
            type: object
        ownToken:
          type: object
          nullable: true
        partnerTokens:
          type: array
          items:
            type: object
        nftCollections:
          type: array
          items:
            type: object
        acceptsUsdc:
          type: boolean

    DiscountBreakdown:
      type: object
      properties:
        symbol:
          type: string
        tier:
          type: string
        discount:
          type: integer
        chain:
          type: string
          description: "Chain name (present only in GET /v1/discount/check responses, omitted in /v1/verify, /v1/acp/discount, /v1/ucp/discount)"

    ComplianceTemplate:
      type: object
      properties:
        provider:
          type: string
          description: "KYC provider name"
          example: "Coinbase"
        description:
          type: string
          description: "Human-readable description of what this template verifies"
          example: "Verified account (KYC completed)"
        chainId:
          type: integer
          description: "EVM chain where the attestation lives"
          example: 8453
        chainName:
          type: string
          description: "Human-readable chain name"
          example: "Base"

paths:
  /v1/jwks:
    get:
      operationId: getJwks
      summary: Get JWKS (public key set)
      description: >
        Returns the JSON Web Key Set containing InsumerAPI's ECDSA P-256 public signing key.
        Use this to verify attestation and trust-profile signatures without hardcoding the key.
        Three `kid` labels are published, all pointing at the SAME key — resolve the public key
        by the `kid` on the response you are verifying: `insumer-attest-v1` (v1 attest and v1
        trust), `insumer-attest-v2` (v2 attest), `insumer-trust-v2` (v2 trust). The `kid` also
        tells you which verification rules to apply (see the `sig` notes on /v1/attest).
        No authentication required. Cached for 24 hours.
      security: []
      responses:
        "200":
          description: JWKS document
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, max-age=86400"
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object
                      properties:
                        kty:
                          type: string
                          example: "EC"
                        crv:
                          type: string
                          example: "P-256"
                        x:
                          type: string
                          description: "Base64url-encoded x coordinate"
                        y:
                          type: string
                          description: "Base64url-encoded y coordinate"
                        use:
                          type: string
                          example: "sig"
                        alg:
                          type: string
                          example: "ES256"
                        kid:
                          type: string
                          description: "One of: insumer-attest-v1, insumer-attest-v2, insumer-trust-v2 (all the same key)."
                          example: "insumer-attest-v1"
                  example:
                    keys:
                      - { kty: EC, crv: P-256, x: "...", y: "...", use: sig, alg: ES256, kid: insumer-attest-v1 }
                      - { kty: EC, crv: P-256, x: "...", y: "...", use: sig, alg: ES256, kid: insumer-attest-v2 }
                      - { kty: EC, crv: P-256, x: "...", y: "...", use: sig, alg: ES256, kid: insumer-trust-v2 }

  /v1/compliance/templates:
    get:
      operationId: listComplianceTemplates
      summary: List compliance templates
      description: >
        Returns pre-configured compliance templates for common KYC/identity providers.
        Templates abstract away raw EAS schema IDs and attester addresses. Use a template
        name in POST /v1/attest conditions instead of specifying schemaId, attester, and
        indexer manually. No authentication required.
      tags: [On-Chain Verification]
      security: []
      responses:
        "200":
          description: Template catalog
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, max-age=3600"
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      templates:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/ComplianceTemplate"
                  meta:
                    $ref: "#/components/schemas/Meta"
              example:
                ok: true
                data:
                  templates:
                    coinbase_verified_account:
                      provider: "Coinbase"
                      description: "Coinbase Verified Account"
                      chainId: 8453
                      chainName: "Base"
                    coinbase_verified_country:
                      provider: "Coinbase"
                      description: "Coinbase Verified Country"
                      chainId: 8453
                      chainName: "Base"
                    coinbase_one:
                      provider: "Coinbase"
                      description: "Coinbase One Member"
                      chainId: 8453
                      chainName: "Base"
                    gitcoin_passport_score:
                      provider: "Gitcoin"
                      description: "Gitcoin Passport Score (≥20)"
                      chainId: 10
                      chainName: "Optimism"
                    gitcoin_passport_active:
                      provider: "Gitcoin"
                      description: "Gitcoin Passport Active"
                      chainId: 10
                      chainName: "Optimism"
                meta:
                  version: "1.0"
                  timestamp: "2026-02-27T12:00:00.000Z"

  /v1/attest:
    post:
      operationId: createAttestation
      summary: Create on-chain verification
      description: >
        Verify 1-10 on-chain conditions (token balances, NFT ownership, EAS attestations).
        Returns ECDSA-signed booleans, never raw balances. 1 credit standard, 2 with proof="merkle".
        EAS: use a compliance template or raw schemaId.
        Authentication: this endpoint accepts EITHER the X-API-Key header OR the Authorization: Wallet
        header (SIWE envelope from a wallet that holds the Insumer Access pass).
      tags: [On-Chain Verification]
      security:
        - ApiKeyAuth: []
        - WalletAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [conditions]
              properties:
                wallet:
                  type: string
                  description: "EVM wallet address (0x...)"
                solanaWallet:
                  type: string
                  description: "Solana wallet address (base58)"
                xrplWallet:
                  type: string
                  description: "XRPL wallet address (r-address, 25-35 chars)"
                  example: "rN7n3473SaZBCG4dFL83w7p1W9cgPJqKro"
                bitcoinWallet:
                  type: string
                  description: "Bitcoin address (P2PKH, P2SH, bech32, or Taproot). Required if any condition uses chainId \"bitcoin\"."
                  example: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
                tronWallet:
                  type: string
                  description: "Tron address (T-prefix, base58, 34 chars). Required if any condition uses chainId \"tron\". Tron supports TRC-20 (e.g. USDT) and native TRX via contractAddress \"native\"."
                  example: "TWdcgk9NLsxKr3z3UELq3vCgsx9wU2cFhB"
                stellarWallet:
                  type: string
                  description: "Stellar address (G-prefix StrKey, 56 chars). Required if any condition uses chainId \"stellar\". Stellar checks measure classic-account balances (trustlines + native XLM); Soroban contract-held balances are out of scope. Trustline conditions require both contractAddress (G-issuer) and assetCode."
                  example: "GB3ZDBVMNFADCPKRVDSTTPW7QQTWD7EAQQ3DCKG74MEALLWYGMPIP6M2"
                suiWallet:
                  type: string
                  description: "Sui address (0x + 64 hex chars, 32-byte). Required if any condition uses chainId \"sui\". Coin type goes in contractAddress (e.g., \"0x2::sui::SUI\" for native, or \"<package>::<module>::<TYPE>\" for any other coin). Response envelope surfaces checkpointSequence as the temporal anchor."
                  example: "0x0000000000000000000000000000000000000000000000000000000000000005"
                proof:
                  type: string
                  enum: ["merkle"]
                  description: >
                    Set to "merkle" to include EIP-1186 Merkle storage proofs in results.
                    Proofs are returned for token_balance conditions on 27 of 31 EVM chains.
                    Ronin, Moonriver, Viction, Solana, XRPL, Bitcoin,
                    and nft_ownership conditions return proof.available: false. Costs 2 credits instead
                    of 1. Note: proof mode reveals the raw on-chain balance to the caller (standard
                    mode never does).
                format:
                  type: string
                  enum: ["jwt"]
                  description: >
                    Set to "jwt" to include a Wallet Auth by InsumerAPI token (ES256-signed JWT) in the response.
                    The JWT contains the attestation data as standard claims (iss, sub, jti, iat, exp)
                    plus pass, results, conditionHash, blockNumber, and blockTimestamp.
                    Verifiable by any standard JWT library using the JWKS at
                    https://insumermodel.com/.well-known/jwks.json. No additional cost.
                conditions:
                  type: array
                  items:
                    $ref: "#/components/schemas/Condition"
                  minItems: 1
                  maxItems: 10
            examples:
              standard:
                summary: Standard attestation (boolean only)
                value:
                  wallet: "0x1234567890abcdef1234567890abcdef12345678"
                  conditions:
                    - type: token_balance
                      contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
                      chainId: 1
                      threshold: "1000"   # decimal string — v2 keys reject a JSON number with 400
                      decimals: 6
                      label: "USDC >= 1000"
                    - type: nft_ownership
                      contractAddress: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"
                      chainId: 1
                      label: "Bored Ape holder"
              eas_template:
                summary: EAS attestation via compliance template
                value:
                  wallet: "0x1234567890abcdef1234567890abcdef12345678"
                  conditions:
                    - type: eas_attestation
                      template: coinbase_verified_account
                      label: "Coinbase KYC verified"
              eas_raw:
                summary: EAS attestation with raw schema ID
                value:
                  wallet: "0x1234567890abcdef1234567890abcdef12345678"
                  conditions:
                    - type: eas_attestation
                      schemaId: "0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9"
                      attester: "0x357458739F90461b99789350868CD7CF330Dd7EE"
                      indexer: "0x2c7eE1E5f416dfF40054c27A62f7B357C4E8619C"
                      chainId: 8453
                      label: "Coinbase Verified Account"
              xrpl:
                summary: XRPL attestation (XRP + RLUSD + USDC)
                value:
                  xrplWallet: "rN7n3473SaZBCG4dFL83w7p1W9cgPJqKro"
                  conditions:
                    - type: token_balance
                      contractAddress: "native"
                      chainId: "xrpl"
                      threshold: "100"
                      label: "XRP >= 100"
                    - type: token_balance
                      contractAddress: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De"
                      chainId: "xrpl"
                      currency: "RLUSD"
                      threshold: "50"
                      label: "RLUSD >= 50"
                    - type: token_balance
                      contractAddress: "rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE"
                      chainId: "xrpl"
                      currency: "USDC"
                      threshold: "100"
                      label: "USDC on XRPL >= 100"
              jwt_format:
                summary: Attestation with Wallet Auth token
                value:
                  wallet: "0x1234567890abcdef1234567890abcdef12345678"
                  format: "jwt"
                  conditions:
                    - type: token_balance
                      contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
                      chainId: 1
                      threshold: "1000"   # decimal string — v2 keys reject a JSON number with 400
                      decimals: 6
                      label: "USDC >= 1000"
              merkle_proof:
                summary: Attestation with Merkle storage proofs
                value:
                  wallet: "0x1234567890abcdef1234567890abcdef12345678"
                  proof: "merkle"
                  conditions:
                    - type: token_balance
                      contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
                      chainId: 1
                      threshold: "1000"   # decimal string — v2 keys reject a JSON number with 400
                      decimals: 6
                      label: "USDC >= 1000"
      responses:
        "200":
          description: Verification created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      attestation:
                        $ref: "#/components/schemas/Attestation"
                      sig:
                        type: string
                        description: >
                          ECDSA P-256 signature (base64, P1363). Branch on `kid` to reconstruct the signed bytes:
                          `insumer-attest-v1` → ES256 over JSON.stringify({id, pass, results, attestedAt});
                          `insumer-attest-v2` → ES256 over the domain-separated, canonical preimage
                          "insumer.attestation.v2\n" + recursive sorted-key canonical JSON of {v:2, id, pass, results, attestedAt} (keys sorted at every level; not full RFC 8785/JCS — safe because all quantities are strings and the only JSON numbers are bounded small integers),
                          where the v2 `results[].evaluatedCondition.threshold` is a canonical decimal string
                          and carries no `decimals` field. v1 stays byte-frozen. Verify with `npm install insumer-verify`.
                      kid:
                        type: string
                        description: "Which key + scheme signed this response. insumer-attest-v1 (v1 attest) or insumer-attest-v2 (v2 attest). Resolve the public key from GET /v1/jwks or https://insumermodel.com/.well-known/jwks.json (all kids share one key)."
                        example: "insumer-attest-v1"
                      jwt:
                        type: string
                        description: "Wallet Auth by InsumerAPI token — ES256 JWT (only present when format: \"jwt\" is requested). Verifiable with any standard JWT library using the JWKS endpoint."
                  meta:
                    type: object
                    properties:
                      creditsRemaining:
                        type: integer
                      creditsCharged:
                        type: integer
                        description: "1 for standard, 2 for proof: merkle"
                      version:
                        type: string
                      timestamp:
                        type: string
                        format: date-time
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: Insufficient verification credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: >
            Data source unavailable — one or more RPC/API calls failed after retries.
            No attestation signed, no credits charged. Retryable after a short delay.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RpcFailureEnvelope"

  /v1/trust:
    post:
      operationId: walletTrust
      summary: Wallet trust fact profile
      description: >
        ECDSA-signed wallet trust profile: 44 base checks across 25 chains in 5 dimensions
        (stablecoins — USDC and USDT, governance, NFTs, staking, institutional_stablecoins —
        EURCV + USDCV + USDC + BENJI across Ethereum, Solana, XRPL, Stellar, and Sui),
        plus optional Solana USDC, XRPL stablecoins (RLUSD + USDC), Bitcoin Holdings (native BTC),
        and Tron USDT — up to 49 total across 27 chains in 9 dimensions. Returns per-check booleans and overall summary.
        3 credits standard, 6 with proof="merkle".
        Authentication: this endpoint accepts EITHER the X-API-Key header OR the Authorization: Wallet
        header (SIWE envelope from a wallet that holds the Insumer Access pass).
      tags: [On-Chain Verification]
      security:
        - ApiKeyAuth: []
        - WalletAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wallet]
              properties:
                wallet:
                  type: string
                  pattern: "^0x[a-fA-F0-9]{40}$"
                  description: "EVM wallet address to profile (required)"
                  example: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
                solanaWallet:
                  type: string
                  description: "Optional Solana wallet address (base58). If provided, adds Solana USDC check."
                xrplWallet:
                  type: string
                  description: "Optional XRPL wallet address (r-address). If provided, adds XRPL stablecoin checks (RLUSD + USDC)."
                bitcoinWallet:
                  type: string
                  description: "Optional Bitcoin address. If provided, adds Bitcoin Holdings dimension (native BTC balance check)."
                tronWallet:
                  type: string
                  description: "Optional Tron address (T-prefix, 34 chars). If provided, adds Tron dimension (USDT-TRC20 check)."
                stellarWallet:
                  type: string
                  description: "Optional Stellar address (G-prefix, 56 chars). If provided, USDC and BENJI on Stellar are evaluated in the institutional_stablecoins dimension. Classic-account balances only — Soroban contracts out of scope."
                suiWallet:
                  type: string
                  description: "Optional Sui address (0x + 64 hex chars). If provided, USDC on Sui is evaluated in the institutional_stablecoins dimension."
                proof:
                  type: string
                  enum: ["merkle"]
                  description: "Set to 'merkle' for EIP-1186 Merkle storage proofs on stablecoin/governance checks. 6 credits."
      responses:
        "200":
          description: Trust fact profile generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      trust:
                        type: object
                        properties:
                          id:
                            type: string
                            description: "Trust profile ID (format: TRST-XXXXX)"
                            example: "TRST-A1B2C"
                          wallet:
                            type: string
                          conditionSetVersion:
                            type: string
                            description: "Version of the curated condition set"
                            example: "v1"
                          dimensions:
                            type: object
                            description: "Checks organized by dimension"
                            properties:
                              stablecoins:
                                $ref: "#/components/schemas/TrustDimension"
                              governance:
                                $ref: "#/components/schemas/TrustDimension"
                              nfts:
                                $ref: "#/components/schemas/TrustDimension"
                              staking:
                                $ref: "#/components/schemas/TrustDimension"
                              solana:
                                description: "Present only when solanaWallet is provided"
                                $ref: "#/components/schemas/TrustDimension"
                              xrpl:
                                description: "Present only when xrplWallet is provided"
                                $ref: "#/components/schemas/TrustDimension"
                          summary:
                            type: object
                            properties:
                              totalChecks:
                                type: integer
                              totalPassed:
                                type: integer
                              totalFailed:
                                type: integer
                              dimensionsWithActivity:
                                type: integer
                              dimensionsChecked:
                                type: integer
                          profiledAt:
                            type: string
                            format: date-time
                          expiresAt:
                            type: string
                            format: date-time
                      sig:
                        type: string
                        description: "ECDSA P-256 signature over the trust object (base64 P1363)"
                      kid:
                        type: string
                        description: "Key ID for JWKS lookup"
                        example: "insumer-attest-v1"
                  meta:
                    type: object
                    properties:
                      creditsRemaining:
                        type: integer
                      creditsCharged:
                        type: integer
                        description: "3 for standard, 6 for proof: merkle"
                      version:
                        type: string
                      timestamp:
                        type: string
              example:
                ok: true
                data:
                  trust:
                    id: "TRST-A1B2C"
                    wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
                    conditionSetVersion: "v1"
                    dimensions:
                      stablecoins:
                        checks:
                          - label: "USDC on Ethereum"
                            chainId: 1
                            met: true
                            evaluatedCondition:
                              type: "token_balance"
                              chainId: 1
                              contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
                              operator: "gt"
                              threshold: 0
                              decimals: 6
                            conditionHash: "0x3a7f..."
                            blockNumber: "0x12f4a80"
                            blockTimestamp: "2026-02-26T12:34:56.000Z"
                        passCount: 3
                        failCount: 23
                        total: 26
                      governance:
                        checks: []
                        passCount: 2
                        failCount: 2
                        total: 4
                      nfts:
                        checks: []
                        passCount: 0
                        failCount: 3
                        total: 3
                      staking:
                        checks: []
                        passCount: 1
                        failCount: 2
                        total: 3
                      institutional_stablecoins:
                        checks: []
                        passCount: 0
                        failCount: 8
                        total: 8
                    summary:
                      totalChecks: 44
                      totalPassed: 6
                      totalFailed: 38
                      dimensionsWithActivity: 3
                      dimensionsChecked: 5
                    profiledAt: "2026-02-26T12:34:57.000Z"
                    expiresAt: "2026-02-26T13:04:57.000Z"
                  sig: "MEYCIQDx...base64..."
                  kid: "insumer-attest-v1"
                meta:
                  creditsRemaining: 47
                  creditsCharged: 3
                  version: "1.0"
                  timestamp: "2026-02-26T12:34:57.000Z"
        "400":
          description: Bad request (missing wallet)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: Insufficient credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: >
            Data source unavailable — one or more RPC/API calls failed after retries.
            No trust profile signed, no credits charged. Retryable after a short delay.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RpcFailureEnvelope"

  /v1/trust/batch:
    post:
      operationId: batchWalletTrust
      summary: Batch wallet trust profiles
      description: >
        Batch trust profiles for up to 10 wallets. 5-8x faster than sequential calls.
        Each wallet gets an independent ECDSA-signed profile. Partial success supported.
        3 credits/wallet standard, 6 with proof="merkle".
        Authentication: this endpoint accepts EITHER the X-API-Key header OR the Authorization: Wallet
        header (SIWE envelope from a wallet that holds the Insumer Access pass).
      tags: [On-Chain Verification]
      security:
        - ApiKeyAuth: []
        - WalletAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wallets]
              properties:
                wallets:
                  type: array
                  minItems: 1
                  maxItems: 10
                  description: "1-10 wallet entries to profile"
                  items:
                    type: object
                    required: [wallet]
                    properties:
                      wallet:
                        type: string
                        pattern: "^0x[a-fA-F0-9]{40}$"
                        description: "EVM wallet address (required)"
                      solanaWallet:
                        type: string
                        description: "Solana wallet address (base58). Adds Solana USDC check for this wallet."
                      xrplWallet:
                        type: string
                        description: "XRPL wallet address (r-address). Adds XRPL stablecoin checks (RLUSD + USDC) for this wallet."
                      bitcoinWallet:
                        type: string
                        description: "Bitcoin address. Adds Bitcoin Holdings dimension (native BTC balance check) for this wallet."
                      tronWallet:
                        type: string
                        description: "Tron address (T-prefix, 34 chars). Adds Tron dimension (USDT-TRC20 check) for this wallet."
                      stellarWallet:
                        type: string
                        description: "Stellar address (G-prefix, 56 chars). Adds Stellar entries (USDC, BENJI) under institutional_stablecoins for this wallet."
                      suiWallet:
                        type: string
                        description: "Sui address (0x + 64 hex chars). Adds Sui USDC under institutional_stablecoins for this wallet."
                proof:
                  type: string
                  enum: ["merkle"]
                  description: "Set to 'merkle' for EIP-1186 Merkle storage proofs on all wallets. 6 credits/wallet."
            example:
              wallets:
                - wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
                - wallet: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
                  solanaWallet: "5v9CXTpN3WHbM2jAYty88qDGz7P4yuMv8fnuPRXBPmiB"
      responses:
        "200":
          description: Batch trust profiles (may include partial failures)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      results:
                        type: array
                        description: "One entry per wallet — either a trust profile or an error"
                        items:
                          oneOf:
                            - type: object
                              properties:
                                trust:
                                  type: object
                                  description: "Full trust profile (same schema as POST /v1/trust)"
                                  properties:
                                    id:
                                      type: string
                                    wallet:
                                      type: string
                                    conditionSetVersion:
                                      type: string
                                    dimensions:
                                      type: object
                                    summary:
                                      type: object
                                    profiledAt:
                                      type: string
                                    expiresAt:
                                      type: string
                                sig:
                                  type: string
                                  description: "ECDSA P-256 signature over this wallet's trust object"
                                kid:
                                  type: string
                            - type: object
                              properties:
                                error:
                                  type: object
                                  properties:
                                    wallet:
                                      type: string
                                    message:
                                      type: string
                      summary:
                        type: object
                        properties:
                          requested:
                            type: integer
                          succeeded:
                            type: integer
                          failed:
                            type: integer
                  meta:
                    type: object
                    properties:
                      creditsCharged:
                        type: integer
                        description: "Total credits charged (successCount × creditsPerWallet)"
                      creditsRemaining:
                        type: integer
                      version:
                        type: string
                      timestamp:
                        type: string
              example:
                ok: true
                data:
                  results:
                    - trust:
                        id: "TRST-A1B2C"
                        wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
                        conditionSetVersion: "v1"
                        dimensions:
                          stablecoins: { passCount: 3, failCount: 23, total: 26 }
                          governance: { passCount: 2, failCount: 2, total: 4 }
                          nfts: { passCount: 0, failCount: 3, total: 3 }
                          staking: { passCount: 1, failCount: 2, total: 3 }
                          institutional_stablecoins: { passCount: 0, failCount: 8, total: 8 }
                        summary: { totalChecks: 44, totalPassed: 6, totalFailed: 38, dimensionsWithActivity: 3, dimensionsChecked: 5 }
                        profiledAt: "2026-02-26T12:34:57.000Z"
                        expiresAt: "2026-02-26T13:04:57.000Z"
                      sig: "MEYCIQDx...base64..."
                      kid: "insumer-attest-v1"
                    - trust:
                        id: "TRST-D4E5F"
                        wallet: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
                        conditionSetVersion: "v1"
                        dimensions:
                          stablecoins: { passCount: 1, failCount: 25, total: 26 }
                          governance: { passCount: 0, failCount: 4, total: 4 }
                          nfts: { passCount: 0, failCount: 3, total: 3 }
                          staking: { passCount: 0, failCount: 3, total: 3 }
                          institutional_stablecoins: { passCount: 0, failCount: 8, total: 8 }
                          solana: { passCount: 1, failCount: 0, total: 1 }
                        summary: { totalChecks: 45, totalPassed: 2, totalFailed: 43, dimensionsWithActivity: 2, dimensionsChecked: 6 }
                      sig: "MEUCIQC...base64..."
                      kid: "insumer-attest-v1"
                  summary: { requested: 2, succeeded: 2, failed: 0 }
                meta: { creditsCharged: 6, creditsRemaining: 44, version: "1.0" }
        "400":
          description: Bad request (invalid wallets array)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: Insufficient credits for entire batch
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/credits:
    get:
      operationId: getCredits
      summary: Check verification credit balance
      description: >
        Returns the verification credit balance, tier, and daily rate limit for the authenticated
        account. Authentication: this endpoint accepts EITHER the X-API-Key header OR the
        Authorization: Wallet header (SIWE envelope from a wallet that holds the Insumer Access pass).
      tags: [On-Chain Verification]
      security:
        - ApiKeyAuth: []
        - WalletAuth: []
      responses:
        "200":
          description: Credit balance
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      apiKeyCredits:
                        type: integer
                      tier:
                        type: string
                        enum: [free, pro, enterprise]
                      dailyLimit:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/keys/buy:
    post:
      operationId: buyKey
      summary: Buy a new API key with USDC, USDT, or BTC (no auth required)
      description: >
        Agent-friendly key purchase: send USDC, USDT, or BTC to the platform wallet
        (EVM USDC/USDT: `0xAd982CB19aCCa2923Df8F687C0614a7700255a23` — same on all chains;
        Solana USDC/USDT: `6a1mLjefhvSJX1sEX8PTnionbE9DqoYjU6F6bNkT4Ydr`;
        Bitcoin BTC: `bc1qg7qnerdhlmdn899zemtez5tcx2a2snc0dt9dt0`), then call this endpoint
        with the transaction hash to receive a new API key with credits. No email or prior
        authentication needed — the sender wallet address from the transaction becomes the key's
        identity. One key per wallet address. Alternative: email-based free-tier signup via
        POST /v1/keys/create (10 free credits, no card). Use POST /v1/credits/buy to top up
        an existing key.
        Volume discounts: $5–$99 = 25 credits/$1 ($0.04/call),
        $100–$499 = 33 credits/$1 ($0.03/call, 25% off), $500+ = 50 credits/$1 ($0.02/call, 50% off).
        USDC and USDT accepted on EVM chains and Solana (auto-detected from transaction).
        BTC accepted on Bitcoin (converted to USD at market rate, requires 1 confirmation).
        Crypto sent on unsupported chains cannot be recovered. All purchases are final and non-refundable.
      tags: [Agent Onboarding]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [txHash, chainId, appName]
              properties:
                txHash:
                  type: string
                  description: "Transaction hash proving payment"
                chainId:
                  $ref: "#/components/schemas/PaymentChainId"
                amount:
                  type: number
                  minimum: 5
                  description: "Stablecoin amount sent (min 5). Not required for BTC — USD value is derived from the on-chain BTC amount at market rate."
                appName:
                  type: string
                  maxLength: 100
                  description: "Name for this API key (e.g. your agent or app name)"
                keyDelivery:
                  type: string
                  enum: [wallet, apiKey, both]
                  default: wallet
                  description: >
                    Preferred credential shape. Default `wallet` returns no API
                    key string when an Insumer Access SBT is minted to the
                    sender wallet — the wallet + pass becomes the credential,
                    and subsequent calls authenticate via Authorization: Wallet.
                    `apiKey` (or its alias `both`) returns the API key string
                    in the response alongside the pass; the wallet still
                    receives the SBT on EVM payments and can use either
                    credential on /v1/attest and /v1/credits/buy. When the
                    SBT cannot land (non-EVM payment, or deferred EVM mint),
                    the API key is returned regardless of this setting so
                    the caller is never left without a usable credential.
                    Case-insensitive.
      responses:
        "200":
          description: API key created with credits
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                      key:
                        type: string
                        description: >
                          Your new API key (store securely — shown only once).
                          CONDITIONALLY PRESENT: omitted when the wallet receives
                          an Insumer Access SBT on this purchase AND the request
                          set `keyDelivery: "wallet"` (the default). In that case
                          the wallet + SBT is the credential and the agent
                          authenticates via Authorization: Wallet. The key is
                          always returned when `keyDelivery` is `apiKey` or
                          `both`, or when the SBT cannot land (non-EVM
                          purchase, or `passMint.status: "deferred"`).
                      authMethod:
                        type: string
                        enum: [wallet, apiKey, both]
                        description: >
                          Indicates which credential(s) were delivered.
                          `wallet` — only the wallet + SBT is the credential
                          (no `key` field in this response).
                          `apiKey` — only the API key string was delivered
                          (no SBT in the wallet; non-EVM payment or deferred mint).
                          `both` — the wallet holds the SBT AND the response
                          carries an API key string; either may be used on
                          wallet-auth-eligible endpoints.
                      authHint:
                        type: string
                        description: >
                          Human-readable guidance for the caller on how to
                          authenticate going forward. Present when `authMethod`
                          is `wallet` or `both`; absent when `authMethod` is
                          `apiKey` (standard X-API-Key usage needs no hint).
                      name:
                        type: string
                      tier:
                        type: string
                      dailyLimit:
                        type: integer
                      creditsAdded:
                        type: integer
                      totalCredits:
                        type: integer
                      usdcPaid:
                        type: string
                      effectiveRate:
                        type: string
                      chainName:
                        type: string
                      registeredWallet:
                        type: string
                        description: "Wallet address registered to this key (extracted from transaction)"
                      passMint:
                        type: object
                        description: >
                          Result of the Insumer Access SBT mint. EVM purchases mint the SBT to
                          the sender wallet on Base mainnet so the wallet can authenticate to
                          /v1/attest via `Authorization: Wallet ...` (SDK: @skyemeta/access).
                          Mint is fail-open — a mint failure does not block the key purchase;
                          the API key + credits are issued regardless. Non-EVM purchases
                          (Solana/BTC/Tron) skip the mint since the sender has no native EVM
                          address; those buyers can use X-API-Key auth, or a future endpoint
                          accepting an explicit EVM mintTo address.
                        properties:
                          status:
                            type: string
                            enum: [minted, already_held, deferred, skipped]
                            description: >
                              `minted` = SBT freshly minted to the sender wallet on Base.
                              `already_held` = wallet already holds a pass (idempotent).
                              `deferred` = mint failed (RPC down, gas, paused contract);
                              caller can retry via a future mint-retry endpoint.
                              `skipped` = non-EVM payment chain (Solana/BTC/Tron).
                          tokenId:
                            type: string
                            nullable: true
                            description: "Token ID of the SBT. May be null on `deferred` or if the post-tx verification read failed (mint receipt still confirmed)."
                          txHash:
                            type: string
                            nullable: true
                            description: "Base transaction hash of the mint. Null on `already_held`, `deferred`, or `skipped`."
                          collection:
                            type: string
                            description: "Insumer Access collection contract address on Base (0x3E2a408cc6eceba04FF9d04A5B8B05aBa8DD50ce)."
                          chainId:
                            type: integer
                            description: "Base mainnet chain ID (8453)."
                          blockNumber:
                            type: string
                            nullable: true
                            description: "Base block number the mint landed in. Null on `already_held`, `deferred`, or `skipped`."
                          reason:
                            type: string
                            description: "Present on `deferred` and `skipped`. Human-readable explanation of why the mint did not happen."
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Key already exists for this wallet, or transaction already used
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: On-chain verification failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/keys/purchase:
    post:
      operationId: purchaseTieredKey
      summary: Buy a tiered prepaid API key with USDC, USDT, or BTC (no auth required)
      description: >
        Email-based tiered key purchase: send the tier-specific amount in USDC/USDT/BTC
        to the platform wallet
        (EVM USDC/USDT: `0xAd982CB19aCCa2923Df8F687C0614a7700255a23` — same on all chains;
        Solana USDC/USDT: `6a1mLjefhvSJX1sEX8PTnionbE9DqoYjU6F6bNkT4Ydr`;
        Bitcoin BTC: `bc1qg7qnerdhlmdn899zemtez5tcx2a2snc0dt9dt0`), then call this endpoint
        with the transaction hash, your email, and the tier you paid for. Returns an API key
        valid for 30 days at the tier's daily limit and credit allocation.
        Fixed pricing: Pro = $29 (10000 requests/day, 100 attest credits),
        Enterprise = $99 (100000 requests/day, 500 attest credits). One-month prepaid; to
        renew, call this endpoint again with a new payment.
        For pay-what-you-want credit purchases without a fixed tier or expiry, see
        POST /v1/keys/buy (wallet-native, agent-friendly).
        Crypto sent on unsupported chains cannot be recovered. All purchases are final and non-refundable.
        Tron is NOT supported on this endpoint (only EVM, Solana, Bitcoin).
        **Response envelope note**: this endpoint is a standalone Cloud Function and
        returns a flat `{success, key, ...}` object — NOT the `{ok, data, meta}` envelope
        used by the main `/v1/*` router.
      tags: [Agent Onboarding]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tier, email, appName, txHash, chainId]
              properties:
                tier:
                  type: string
                  enum: [pro, enterprise]
                  description: "Pricing tier. Pro = $29, Enterprise = $99 (USD-equivalent for BTC payments)."
                email:
                  type: string
                  format: email
                  description: "Owner email for account recovery and renewal reminders."
                appName:
                  type: string
                  maxLength: 100
                  description: "Name for this API key (e.g., your app or service name)."
                txHash:
                  type: string
                  description: "Transaction hash proving payment of the tier-specific amount."
                chainId:
                  oneOf:
                    - type: integer
                    - type: string
                      enum: [solana, bitcoin]
                  description: "EVM chain ID, or 'solana' / 'bitcoin'. Tron not supported on this endpoint."
      responses:
        "200":
          description: API key created with 30-day validity at the tier's limits
          content:
            application/json:
              schema:
                type: object
                description: "Flat response — not wrapped in {ok, data, meta} envelope."
                properties:
                  success:
                    type: boolean
                    const: true
                  key:
                    type: string
                    description: "Your new API key (store securely — shown only once)."
                  tier:
                    type: string
                    enum: [pro, enterprise]
                  dailyLimit:
                    type: integer
                    description: "Pro = 10000, Enterprise = 100000."
                  apiKeyCredits:
                    type: integer
                    description: "Initial attestation credits. Pro = 100, Enterprise = 500."
                  expiresAt:
                    type: string
                    format: date-time
                    description: "ISO 8601 timestamp 30 days from purchase. Key stops authenticating after this."
                  validDays:
                    type: integer
                    const: 30
        "400":
          description: Validation error (missing fields, invalid tier, malformed email, unsupported chainId)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        "405":
          description: Method not allowed (only POST supported)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        "409":
          description: Transaction hash already used (replay protection)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        "422":
          description: On-chain verification failed (wrong amount, wrong recipient, tx not found, insufficient confirmations)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string

  /v1/credits/buy:
    post:
      operationId: buyCredits
      summary: Buy verification credits with USDC, USDT, or BTC
      description: >
        Purchase verification credits by sending USDC, USDT, or BTC to the platform wallet
        (EVM USDC/USDT: `0xAd982CB19aCCa2923Df8F687C0614a7700255a23` — same on all chains;
        Solana USDC/USDT: `6a1mLjefhvSJX1sEX8PTnionbE9DqoYjU6F6bNkT4Ydr`;
        Bitcoin BTC: `bc1qg7qnerdhlmdn899zemtez5tcx2a2snc0dt9dt0`) and providing
        the transaction hash. Volume discounts: $5–$99 = 25 credits/$1 ($0.04/call),
        $100–$499 = 33 credits/$1 ($0.03/call, 25% off), $500+ = 50 credits/$1 ($0.02/call, 50% off).
        USDC and USDT accepted on EVM chains and Solana (auto-detected from transaction).
        BTC accepted on Bitcoin (converted to USD at market rate, requires 1 confirmation).
        Crypto sent on unsupported chains cannot be recovered. All purchases are final and non-refundable.
        Sender verification: the first purchase registers the sender wallet address to the API key.
        Subsequent purchases must come from the same sender wallet.
        To change the registered wallet, include `"updateWallet": true` — the verified transfer
        from the new address proves ownership. `updateWallet` is ignored for wallet-auth callers
        (the SIWE signer is already bound to the registered wallet, and the soulbound pass cannot
        follow a wallet rotation).
        Authentication: this endpoint accepts EITHER the X-API-Key header OR the Authorization: Wallet
        header (SIWE envelope from a wallet that holds the Insumer Access pass). Wallet-auth callers
        let an SBT-holding agent top up without ever handling an API key.
      tags: [On-Chain Verification]
      security:
        - ApiKeyAuth: []
        - WalletAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [txHash, chainId]
              properties:
                txHash:
                  type: string
                  description: "Transaction hash proving payment"
                chainId:
                  $ref: "#/components/schemas/PaymentChainId"
                amount:
                  type: number
                  minimum: 5
                  description: "Stablecoin amount sent (min 5). Not required for BTC."
                updateWallet:
                  type: boolean
                  default: false
                  description: "Set true to replace the registered sender wallet with this transaction's sender"
      responses:
        "200":
          description: Credits purchased
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      creditsAdded:
                        type: integer
                      totalCredits:
                        type: integer
                      usdcPaid:
                        type: string
                      chainName:
                        type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Transaction already used
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: On-chain verification failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants:
    get:
      operationId: listMerchants
      summary: List merchants in public directory
      description: "Browse merchants with optional filters. Returns public directory listings."
      tags: [Discovery]
      security: []
      parameters:
        - name: token
          in: query
          schema:
            type: string
          description: "Filter by accepted token symbol (e.g. UNI)"
        - name: verified
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: "Filter by domain verification status"
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
          description: "Results per page"
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
          description: "Pagination offset"
      responses:
        "200":
          description: Merchant list
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/MerchantSummary"
                  meta:
                    type: object
                    properties:
                      total:
                        type: integer
                      limit:
                        type: integer
                      offset:
                        type: integer
                      version:
                        type: string
                      timestamp:
                        type: string
                        format: date-time
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

    post:
      operationId: createMerchant
      summary: Create a new merchant
      description: >
        Create a merchant programmatically. Receives 100 free verification credits.
        The API key that creates the merchant owns it. Max 10 merchants per API key.
      tags: [Agent Onboarding]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [companyName, companyId]
              properties:
                companyName:
                  type: string
                  maxLength: 100
                  description: "Display name"
                companyId:
                  type: string
                  minLength: 2
                  maxLength: 50
                  pattern: "^[a-zA-Z0-9_-]+$"
                  description: "Unique ID (alphanumeric, dash, underscore)"
                location:
                  type: string
                  maxLength: 200
                  description: "City or region"
                externalId:
                  type: string
                  maxLength: 100
                  description: >
                    Optional partner handle for this merchant (e.g. a token collection
                    address). Makes the call an idempotent upsert and requires domain
                    verification before the merchant can publish to the directory.
            example:
              companyName: "Acme Coffee"
              companyId: "ACME-COFFEE"
              location: "New York, NY"
      responses:
        "200":
          description: >
            Merchant already exists and is managed by this same API key — returned as an
            idempotent upsert (the existing merchant) instead of a 409.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      companyName:
                        type: string
                      credits:
                        type: integer
                      apiAccessEnabled:
                        type: boolean
                      alreadyExists:
                        type: boolean
                        const: true
                  meta:
                    $ref: "#/components/schemas/Meta"
        "201":
          description: Merchant created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      companyName:
                        type: string
                      credits:
                        type: integer
                        example: 100
                      apiAccessEnabled:
                        type: boolean
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Merchant ID already exists and is owned by a different API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Max 10 merchants per API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}:
    get:
      operationId: getMerchant
      summary: Get merchant details
      description: "Returns full public merchant profile including token tiers and NFT collections."
      tags: [Discovery]
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      responses:
        "200":
          description: Merchant details
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    $ref: "#/components/schemas/MerchantDetail"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Invalid merchant ID format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/tokens:
    get:
      operationId: listTokens
      summary: List registered tokens and NFTs
      description: "Returns all tokens and NFT collections in the registry, with optional filters."
      tags: [Discovery]
      security: []
      parameters:
        - name: chain
          in: query
          schema:
            $ref: "#/components/schemas/ChainId"
          description: "Filter by chain ID"
        - name: symbol
          in: query
          schema:
            type: string
          description: "Filter by token symbol"
        - name: type
          in: query
          schema:
            type: string
            enum: [token, nft]
          description: "Filter by asset type"
      responses:
        "200":
          description: Token and NFT registry
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      tokens:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                            symbol:
                              type: string
                            name:
                              type: string
                            chainId:
                              $ref: "#/components/schemas/ChainId"
                            chainName:
                              type: string
                            contractAddress:
                              type: string
                            decimals:
                              type: integer
                            logo:
                              type: string
                      nfts:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                            name:
                              type: string
                            contractAddress:
                              type: string
                            chainId:
                              $ref: "#/components/schemas/ChainId"
                            chainName:
                              type: string
                            image:
                              type: string
                            standard:
                              type: string
                  meta:
                    type: object
                    properties:
                      tokenCount:
                        type: integer
                      nftCount:
                        type: integer
                      version:
                        type: string
                      timestamp:
                        type: string
                        format: date-time
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/discount/check:
    get:
      operationId: checkDiscount
      summary: Calculate discount for wallet at merchant
      description: >
        Checks on-chain balances server-side and calculates the total discount.
        Free to call — does not consume merchant credits. Returns tier and discount
        per token, but never raw balance amounts.
      tags: [Discount Verification]
      security: []
      parameters:
        - name: wallet
          in: query
          schema:
            type: string
          description: "EVM wallet address (0x...)"
        - name: solanaWallet
          in: query
          schema:
            type: string
          description: "Solana wallet address (base58)"
        - name: xrplWallet
          in: query
          schema:
            type: string
          description: "XRPL wallet address (r-address)"
        - name: merchant
          in: query
          required: true
          schema:
            type: string
          description: "Merchant ID"
      responses:
        "200":
          description: Discount calculation
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      eligible:
                        type: boolean
                      totalDiscount:
                        type: integer
                      discountMode:
                        type: string
                        enum: [highest, stack, capped]
                      breakdown:
                        type: array
                        items:
                          $ref: "#/components/schemas/DiscountBreakdown"
                      merchantId:
                        type: string
                      merchantName:
                        type: string
                      chainsChecked:
                        type: array
                        items:
                          type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Missing or invalid parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/verify:
    post:
      operationId: createVerification
      summary: Create signed discount code
      description: >
        Creates a signed discount verification code (INSR-XXXXX) valid for 30 minutes.
        Consumes 1 merchant credit. If merchant has Stripe Connect, a coupon is auto-created.
      tags: [Discount Verification]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [merchantId]
              properties:
                merchantId:
                  type: string
                wallet:
                  type: string
                  description: "EVM wallet address"
                solanaWallet:
                  type: string
                  description: "Solana wallet address"
                xrplWallet:
                  type: string
                  description: "XRPL wallet address (r-address)"
      responses:
        "200":
          description: Verification code created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      verified:
                        type: boolean
                      totalDiscount:
                        type: integer
                      code:
                        type: string
                        description: "Format: INSR-XXXXX"
                        example: "INSR-A7K3M"
                      expiresAt:
                        type: string
                        format: date-time
                      breakdown:
                        type: array
                        items:
                          $ref: "#/components/schemas/DiscountBreakdown"
                      stripeCodeCreated:
                        type: boolean
                      cloverDiscountName:
                        type: string
                        nullable: true
                      merchantName:
                        type: string
                      sig:
                        type: string
                        description: "ECDSA P-256 signature"
                      usdcPayment:
                        type: object
                        nullable: true
                        description: "Present when merchant has USDC payments enabled"
                        properties:
                          evmAddress:
                            type: string
                          preferredChainId:
                            type: integer
                          preferredChainName:
                            type: string
                          usdcContract:
                            type: string
                          solanaAddress:
                            type: string
                            nullable: true
                          supportedChains:
                            type: array
                            items:
                              type: object
                              properties:
                                chainId:
                                  type: integer
                                name:
                                  type: string
                                usdcContract:
                                  type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: No merchant credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: Merchant has not enabled API access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: >
            Data source unavailable — one or more RPC/API calls failed after retries.
            No discount code signed, no credits charged. Retryable after a short delay.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RpcFailureEnvelope"

  /v1/payment/confirm:
    post:
      operationId: confirmPayment
      summary: Verify USDC payment for discount code
      description: >
        After /v1/verify, confirm that a USDC payment was made on-chain.
        The server checks the transaction receipt to verify USDC arrived at the merchant address.
      tags: [Discount Verification]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, txHash, chainId, amount]
              properties:
                code:
                  type: string
                  description: "Verification code (e.g. INSR-A7K3M)"
                txHash:
                  type: string
                  description: "On-chain transaction hash or Solana signature"
                chainId:
                  $ref: "#/components/schemas/UsdcChainId"
                amount:
                  oneOf:
                    - type: string
                    - type: number
                  description: "USDC amount sent"
      responses:
        "200":
          description: Payment confirmed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      confirmed:
                        type: boolean
                      code:
                        type: string
                      txHash:
                        type: string
                      chainId:
                        $ref: "#/components/schemas/UsdcChainId"
                      chainName:
                        type: string
                      amountVerified:
                        type: string
                      confirmedAt:
                        type: string
                        format: date-time
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: Merchant USDC payments not enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Code or merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Payment already confirmed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "410":
          description: Code expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: On-chain verification failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/tokens:
    put:
      operationId: updateMerchantTokens
      summary: Configure merchant token tiers
      description: >
        Set up the merchant's own token and/or partner tokens with discount tiers.
        Max 8 tokens total (own + partner). Must be called by the API key that created the merchant.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ownToken:
                  oneOf:
                    - $ref: "#/components/schemas/TokenConfig"
                    - type: "null"
                  description: "Merchant's own token config, or null to disable"
                partnerTokens:
                  type: array
                  items:
                    $ref: "#/components/schemas/TokenConfig"
                  description: "Partner token configs"
            example:
              ownToken:
                symbol: "ACME"
                chainId: 8453
                contractAddress: "0x1234567890abcdef1234567890abcdef12345678"
                decimals: 18
                tiers:
                  - name: "Bronze"
                    threshold: 100
                    discount: 5
                  - name: "Silver"
                    threshold: 1000
                    discount: 10
                  - name: "Gold"
                    threshold: 10000
                    discount: 15
              partnerTokens: []
      responses:
        "200":
          description: Tokens configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      ownToken:
                        type: object
                        nullable: true
                        properties:
                          symbol:
                            type: string
                          tiersConfigured:
                            type: integer
                      partnerTokens:
                        type: array
                        items:
                          type: object
                      totalTokens:
                        type: integer
                      maxTokens:
                        type: integer
                        example: 8
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/nfts:
    put:
      operationId: updateMerchantNfts
      summary: Configure NFT collections
      description: >
        Set NFT collections that grant discounts. Max 4 collections.
        Must be called by the API key that created the merchant.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nftCollections]
              properties:
                nftCollections:
                  type: array
                  items:
                    $ref: "#/components/schemas/NftConfig"
                  maxItems: 4
            example:
              nftCollections:
                - name: "Acme Founders"
                  contractAddress: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"
                  chainId: 1
                  discount: 15
      responses:
        "200":
          description: NFTs configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      nftCollections:
                        type: array
                        items:
                          type: object
                          properties:
                            name:
                              type: string
                            discount:
                              type: integer
                            chainId:
                              $ref: "#/components/schemas/ChainId"
                      totalCollections:
                        type: integer
                      maxCollections:
                        type: integer
                        example: 4
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/settings:
    put:
      operationId: updateMerchantSettings
      summary: Update merchant settings
      description: >
        Update discount stacking mode, cap, and USDC payment settings.
        All fields optional. Must be called by the API key that created the merchant.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                discountMode:
                  type: string
                  enum: [highest, stack, capped]
                  description: "'highest' uses best single discount, 'stack' sums all, 'capped' sums all then caps at discountCap"
                discountCap:
                  type: integer
                  minimum: 1
                  maximum: 100
                  description: "Maximum total discount percentage"
                usdcPayment:
                  oneOf:
                    - type: object
                      properties:
                        enabled:
                          type: boolean
                        evmAddress:
                          type: string
                          description: "EVM wallet for USDC (0x + 40 hex)"
                        solanaAddress:
                          type: string
                          description: "Solana wallet for USDC (base58)"
                        preferredChainId:
                          $ref: "#/components/schemas/UsdcChainId"
                    - type: "null"
                  description: "USDC payment config, or null to disable"
      responses:
        "200":
          description: Settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      discountMode:
                        type: string
                      discountCap:
                        type: integer
                      usdcPayment:
                        type: object
                        nullable: true
                        properties:
                          enabled:
                            type: boolean
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error or no fields provided
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/credits:
    post:
      operationId: buyMerchantCredits
      summary: Buy merchant verification credits with USDC, USDT, or BTC
      description: >
        Purchase merchant verification credits (consumed by /v1/verify).
        Volume discounts: $5–$99 = 25 credits/$1 ($0.04/call),
        $100–$499 = 33 credits/$1 ($0.03/call), $500+ = 50 credits/$1 ($0.02/call).
        Send to the platform wallet: EVM USDC/USDT
        `0xAd982CB19aCCa2923Df8F687C0614a7700255a23` (same on all chains),
        Solana USDC/USDT `6a1mLjefhvSJX1sEX8PTnionbE9DqoYjU6F6bNkT4Ydr`,
        Bitcoin BTC `bc1qg7qnerdhlmdn899zemtez5tcx2a2snc0dt9dt0`.
        USDC and USDT auto-detected from EVM and Solana transactions.
        BTC accepted on Bitcoin (converted to USD at market rate, requires 1 confirmation).
        Supported chains: Ethereum, Base, Polygon, Arbitrum, Optimism, BNB Chain, Avalanche, Solana, Bitcoin.
        Crypto sent on unsupported chains cannot be recovered. All purchases are final and non-refundable.
        Sender verification: the first purchase registers the sender wallet address to the API key.
        Subsequent purchases must come from the same sender wallet.
        To change the registered wallet, include `"updateWallet": true` — the verified transfer
        from the new address proves ownership.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [txHash, chainId]
              properties:
                txHash:
                  type: string
                  description: "Transaction hash proving payment"
                chainId:
                  $ref: "#/components/schemas/PaymentChainId"
                amount:
                  type: number
                  minimum: 5
                  description: "Stablecoin amount sent (min 5). Not required for BTC — USD value derived from on-chain BTC amount at market rate."
                updateWallet:
                  type: boolean
                  default: false
                  description: "Set true to replace the registered sender wallet with this transaction's sender"
      responses:
        "200":
          description: Credits purchased
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      creditsAdded:
                        type: integer
                      totalCredits:
                        type: integer
                      usdcPaid:
                        type: string
                      chainName:
                        type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: Transaction already used
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: On-chain verification failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/directory:
    post:
      operationId: publishToDirectory
      summary: Publish or unpublish merchant in public directory
      description: >
        Publish (or refresh) the merchant's listing in the public directory.
        No request body needed to publish — call again after updating tokens/settings.
        Send `{ "listed": false }` to unpublish (delist); the company and its tokens
        stay intact, only the public entry is removed. Partner-provisioned merchants
        (created with an externalId) must pass domain verification before they can
        publish — an unverified one is rejected with 403.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                listed:
                  type: boolean
                  description: "Set to false to unpublish (delist) the merchant. Omit to publish/refresh."
            example:
              listed: false
      responses:
        "200":
          description: Published, or unpublished when listed=false
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      published:
                        type: boolean
                      tokensListed:
                        type: integer
                      nftCollectionsListed:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: >
            API key does not own this merchant, or a partner-provisioned merchant
            (externalId set) has not passed domain verification yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/status:
    get:
      operationId: getMerchantStatus
      summary: Get full private merchant details
      description: >
        Returns full details including credits, token configs, directory status,
        verification status, and USDC settings. Only accessible by the API key that created the merchant.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      responses:
        "200":
          description: Full merchant status
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      companyName:
                        type: string
                      location:
                        type: string
                      credits:
                        type: integer
                      apiAccessEnabled:
                        type: boolean
                      discountMode:
                        type: string
                        enum: [highest, stack, capped]
                      discountCap:
                        type: integer
                      listedInDirectory:
                        type: boolean
                      ownToken:
                        type: object
                        nullable: true
                      partnerTokens:
                        type: array
                        items:
                          type: object
                      nftCollections:
                        type: array
                        items:
                          type: object
                      usdcPayment:
                        type: object
                        nullable: true
                        properties:
                          enabled:
                            type: boolean
                          evmAddress:
                            type: string
                          solanaAddress:
                            type: string
                            nullable: true
                      verification:
                        type: object
                        description: "Domain verification status (token is never exposed here)"
                        properties:
                          status:
                            type: string
                            enum: [unverified, pending, verified]
                          domain:
                            type: string
                            nullable: true
                          method:
                            type: string
                            nullable: true
                            enum: [dns, meta, file]
                          verifiedAt:
                            type: string
                            format: date-time
                            nullable: true
                      createdAt:
                        type: object
                        description: "Firestore timestamp object"
                        properties:
                          _seconds:
                            type: integer
                          _nanoseconds:
                            type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/domain-verification:
    post:
      operationId: requestDomainVerification
      summary: Request domain verification token
      description: >
        Generates a verification token for proving domain ownership. The token can be placed
        via DNS TXT record, HTML meta tag, or a verification file. Each POST regenerates the
        token, invalidating any previous one. Only the API key that created the merchant can call this.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: "Domain to verify (e.g. example.com). No protocol prefix."
            example:
              domain: "acme-coffee.com"
      responses:
        "200":
          description: Verification token generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      domain:
                        type: string
                      token:
                        type: string
                        description: "Verification token (insumer-XXXXXXXXXXXXXXXX)"
                      methods:
                        type: object
                        properties:
                          dns:
                            type: object
                            properties:
                              type:
                                type: string
                                example: "TXT"
                              host:
                                type: string
                              value:
                                type: string
                              instructions:
                                type: string
                          meta:
                            type: object
                            properties:
                              tag:
                                type: string
                              instructions:
                                type: string
                          file:
                            type: object
                            properties:
                              path:
                                type: string
                              content:
                                type: string
                              instructions:
                                type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Invalid domain format or missing domain
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

    put:
      operationId: verifyDomain
      summary: Trigger domain verification check
      description: >
        Checks all three verification methods (DNS TXT, meta tag, file) for the previously
        requested domain. Rate limited to 5 attempts per hour per merchant. If already verified,
        returns the existing verification result. Only the API key that created the merchant can call this.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      verified:
                        type: boolean
                      domain:
                        type: string
                      method:
                        type: string
                        enum: [dns, meta, file]
                        description: "Present only when verified is true"
                      verifiedAt:
                        type: string
                        format: date-time
                        description: "Present only when verified is true"
                      message:
                        type: string
                        description: "Present only when verified is false"
                      attemptsRemaining:
                        type: integer
                        description: "Present only when verified is false"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: No verification pending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded (5 attempts per hour)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/verification:
    put:
      operationId: partnerAssertDomain
      summary: White-label partner asserts domain ownership
      description: >
        Record that a managing white-label partner verified an organization's domain
        in its own branded flow, so the org can publish to the directory without ever
        seeing an Insumer-branded challenge. Sets verification.status=verified with
        method=partner. RESTRICTED to designated white-label partner keys — a normal
        self-serve key cannot assert verification this way and must use the
        domain-verification challenge flow (POST/PUT /v1/merchants/{id}/domain-verification).
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: "Domain the partner verified (no protocol prefix)."
            example:
              domain: "acme-coffee.com"
      responses:
        "200":
          description: Domain marked verified
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      verified:
                        type: boolean
                        const: true
                      domain:
                        type: string
                      method:
                        type: string
                        const: partner
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: A valid domain is required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: Key is not a white-label partner key, or does not manage this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/registry/submit:
    post:
      operationId: submitToRegistry
      summary: Submit a token or NFT to the public registry (pending review)
      description: >
        Submit a token or NFT collection to the public registry as PENDING for human
        approval. X-API-Key required (free — no credits are charged; the key is only for
        accountability). Nothing goes live until approved. The submission is routed into
        one of two buckets by asset type: Tokens — ERC-20 (utility) or ERC-3643 (security),
        plus Solana SPL, etc. — and NFTs — ERC-721/1155, incl. ERC-5192 soulbound. Set
        `assetType` ("token"/"nft") explicitly, or it is inferred from `standard`
        (defaulting to NFT). A token submission requires `symbol`. Idempotent per chain +
        address: a re-submit of the same asset returns the existing entry rather than
        creating a duplicate. EVM addresses are normalized lowercase; Solana mint addresses
        are left as-is. Returns a flat envelope (no `data` wrapper). Rate limit: 200 per window.
      tags: [Agent Onboarding]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, chainId]
              properties:
                name:
                  type: string
                  maxLength: 200
                assetType:
                  type: string
                  enum: [token, nft]
                  description: >
                    Which registry bucket to route into. "token" → tokenRegistry (ERC-20,
                    ERC-3643, SPL, …); "nft" → nftRegistry (ERC-721/1155/5192). If omitted,
                    inferred from `standard`, defaulting to "nft". A "token" submission
                    requires `symbol`.
                contractAddress:
                  type: string
                  maxLength: 200
                  description: "EVM contract address. Provide this or mintAddress."
                mintAddress:
                  type: string
                  maxLength: 200
                  description: "Solana mint address. Provide this or contractAddress."
                chainId:
                  oneOf:
                    - type: integer
                    - type: string
                  description: "Positive integer for EVM, or the string \"solana\"."
                chainName:
                  type: string
                  maxLength: 50
                symbol:
                  type: string
                  maxLength: 20
                  description: "Ticker symbol. Required for token submissions; ignored for NFTs."
                decimals:
                  type: integer
                  description: "Token decimals (token submissions only)."
                image:
                  type: string
                  maxLength: 500
                  description: "Collection image URL (NFT submissions). For tokens, use `logo`."
                logo:
                  type: string
                  maxLength: 500
                  description: "Token logo URL (token submissions). `image` is accepted as a fallback."
                standard:
                  type: string
                  maxLength: 50
                  description: "e.g. ERC-721, ERC-20, ERC-3643. Used to infer `assetType` when it is omitted."
                website:
                  type: string
                  maxLength: 500
                description:
                  type: string
                  maxLength: 1000
                coingeckoId:
                  type: string
                  maxLength: 100
                  description: "CoinGecko id for market-cap lookup (token submissions only)."
                source:
                  type: string
                  maxLength: 50
                  description: "Provenance tag shown in the admin review queue (e.g. \"partner-app\")."
            examples:
              nft:
                summary: NFT collection (ERC-721)
                value:
                  name: "Acme Founders"
                  assetType: "nft"
                  contractAddress: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"
                  chainId: 8453
                  chainName: "Base"
                  standard: "ERC-721"
                  source: "partner-app"
              token:
                summary: Security token (ERC-3643)
                value:
                  name: "Acme Equity"
                  assetType: "token"
                  symbol: "ACME"
                  contractAddress: "0x80dE9bCc7540DCcb9cD7826F9dd19099B876aF54"
                  chainId: 8453
                  chainName: "Base"
                  decimals: 18
                  standard: "ERC-3643"
                  source: "partner-app"
      responses:
        "201":
          description: Submitted (pending review)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  pending:
                    type: boolean
                    const: true
                  docId:
                    type: string
                  assetType:
                    type: string
                    enum: [token, nft]
                    description: "Which bucket the submission was routed into."
                  message:
                    type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "200":
          description: Already listed (idempotent)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  alreadyListed:
                    type: boolean
                    const: true
                  pending:
                    type: boolean
                  active:
                    type: boolean
                  docId:
                    type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Missing name/contractAddress/chainId, or invalid chainId
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Submission limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/merchants/{id}/scanners:
    post:
      operationId: manageMerchantScanners
      summary: Add or remove an in-person scanner PIN
      description: >
        Add or remove a scanner PIN on a merchant you manage, so a partner can run a
        login-less branded scanner with no Firebase account. Only the API key that created
        the merchant can call this. Max 10 scanners per merchant. PINs are stored salted +
        hashed and are never returned. Send `{ name?, pin }` to add, or `{ remove: scannerId }`
        to remove.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [pin]
                  properties:
                    name:
                      type: string
                      maxLength: 50
                      description: "Scanner label (defaults to \"Register\")."
                    pin:
                      type: string
                      pattern: "^\\d{4,8}$"
                      description: "4-8 digit PIN."
                - type: object
                  required: [remove]
                  properties:
                    remove:
                      type: string
                      description: "Scanner id to remove."
            examples:
              add:
                value: { name: "Front Register", pin: "4821" }
              remove:
                value: { remove: "a1b2c3d4e5f6" }
      responses:
        "201":
          description: Scanner added
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      totalScanners:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "200":
          description: Scanner removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      removed:
                        type: string
                      totalScanners:
                        type: integer
                      changed:
                        type: boolean
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: PIN not 4-8 digits, or max 10 scanners reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    get:
      operationId: listMerchantScanners
      summary: List a merchant's scanners
      description: >
        List the scanners configured on a merchant you manage. Owner key only. Returns
        id and name only — PINs are never exposed.
      tags: [Agent Onboarding]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: "Merchant ID"
      responses:
        "200":
          description: Scanner list
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      scanners:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                            name:
                              type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/scanner/session:
    post:
      operationId: scannerSession
      summary: Validate a scanner PIN and return merchant config
      description: >
        Validate a scanner PIN and return the merchant config for a login-less branded
        scanner. Owner key only (the partner key that provisioned the merchant). Lets a
        scanner with no Firebase login authenticate with just the merchant id + a PIN.
        Brute-force guard: 5 failed attempts per 15 minutes per merchant.
      tags: [Agent Onboarding]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [merchantId, pin]
              properties:
                merchantId:
                  type: string
                pin:
                  type: string
                  description: "4-8 digit PIN."
            example:
              merchantId: "acme-coffee"
              pin: "4821"
      responses:
        "200":
          description: PIN valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      valid:
                        type: boolean
                        const: true
                      scannerId:
                        type: string
                      scannerName:
                        type: string
                      merchantName:
                        type: string
                      discountMode:
                        type: string
                        enum: [highest, stack, capped]
                      discountCap:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Missing merchantId or pin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid scanner code or PIN
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: API key does not own this merchant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Too many failed PIN attempts (5 per 15 minutes)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/pass/verify:
    post:
      operationId: verifyPass
      summary: Verify a scanned pass and compute the discount
      description: >
        Verify a scanned pass and compute the discount. X-API-Key required (metered on the
        partner key). All cryptography stays server-side: InsumerAPI verifies the ECDSA
        signature on the scanned QR, enforces 65-second freshness, then computes the discount
        against the merchant config. The partner's scanner only displays the result. A bad
        signature is rejected with no discount. There is deliberately no companion "sign"
        endpoint — passes are issued by InsumerAPI's own pass surface; signing is the
        primitive and is never exposed as an API call.
      tags: [Agent Onboarding]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [merchantId, qr]
              properties:
                merchantId:
                  type: string
                qr:
                  type: object
                  description: >
                    The scanned pass object exactly as encoded, including its `sig`. The
                    signed bytes are the QR object with `sig` removed, re-serialized
                    (`sig` is always the last key).
                  properties:
                    w:
                      type: string
                      description: "Truncated wallet address."
                    t:
                      type: string
                      description: "Encoded token balances."
                    nft:
                      type: string
                      description: "Encoded NFT holdings."
                    ts:
                      type: integer
                      description: "Issue timestamp in ms (65-second freshness window)."
                    n:
                      type: string
                      description: "Nonce."
                    sig:
                      type: string
                      description: "Base64 P1363 ECDSA signature."
                  required: [sig]
            example:
              merchantId: "acme-coffee"
              qr:
                w: "0x1234…5678"
                t: "ACME:1500.00@8453"
                ts: 1749150000000
                n: "a1b2c3"
                sig: "<base64 P1363 ECDSA signature>"
      responses:
        "200":
          description: Pass verified
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      recognized:
                        type: boolean
                      discount:
                        type: integer
                      breakdown:
                        type: array
                        items:
                          type: object
                          properties:
                            symbol:
                              type: string
                            tier:
                              type: string
                            discount:
                              type: integer
                      merchantName:
                        type: string
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Missing merchantId, or qr/sig
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid signature or expired pass
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /v1/acp/discount:
    post:
      operationId: acpDiscount
      summary: ACP-format discount eligibility check
      description: >
        ACP discount check. Same verification as POST /v1/verify, wrapped in OpenAI/Stripe Agentic Commerce Protocol
        format (coupon objects, allocations, applied/rejected arrays). 1 merchant credit. Source: "acp".
      tags: [Commerce Protocol Integration]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [merchantId]
              properties:
                merchantId:
                  type: string
                  description: "Merchant identifier"
                wallet:
                  type: string
                  description: "EVM wallet address (0x...)"
                solanaWallet:
                  type: string
                  description: "Solana wallet address"
                xrplWallet:
                  type: string
                  description: "XRPL wallet address (r-address)"
                items:
                  type: array
                  description: "Optional line items for per-item cent-amount allocations"
                  items:
                    type: object
                    properties:
                      path:
                        type: string
                        description: "JSONPath reference to the line item"
                        example: "$.line_items[0]"
                      amount:
                        type: integer
                        description: "Item price in cents"
                        example: 2500
      responses:
        "200":
          description: ACP-format discount response
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      protocol:
                        type: string
                        const: "acp"
                      version:
                        type: string
                        example: "2026-01-30"
                      discounts:
                        type: object
                        properties:
                          codes:
                            type: array
                            items:
                              type: string
                          applied:
                            type: array
                            items:
                              type: object
                              properties:
                                id:
                                  type: string
                                code:
                                  type: string
                                coupon:
                                  type: object
                                  properties:
                                    id:
                                      type: string
                                    name:
                                      type: string
                                    percent_off:
                                      type: integer
                                amount:
                                  type: integer
                                  nullable: true
                                automatic:
                                  type: boolean
                                method:
                                  type: string
                                priority:
                                  type: integer
                                allocations:
                                  type: array
                                  items:
                                    type: object
                                    properties:
                                      path:
                                        type: string
                                      amount:
                                        type: integer
                                start:
                                  type: string
                                  format: date-time
                                end:
                                  type: string
                                  format: date-time
                          rejected:
                            type: array
                            items:
                              type: object
                              properties:
                                code:
                                  type: string
                                reason:
                                  type: string
                      verification:
                        type: object
                        properties:
                          code:
                            type: string
                          expiresAt:
                            type: string
                            format: date-time
                          breakdown:
                            type: array
                            items:
                              $ref: "#/components/schemas/DiscountBreakdown"
                          sig:
                            type: string
                          kid:
                            type: string
                  meta:
                    type: object
                    properties:
                      creditsRemaining:
                        type: integer
                      creditsCharged:
                        type: integer
                        description: "Always 1 for ACP discount checks"
                      version:
                        type: string
                      timestamp:
                        type: string
                        format: date-time
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: No merchant credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: >
            Data source unavailable — retryable after a short delay.
            No discount signed, no credits charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RpcFailureEnvelope"

  /v1/ucp/discount:
    post:
      operationId: ucpDiscount
      summary: UCP-format discount eligibility check
      description: >
        UCP discount check. Same verification as POST /v1/verify, wrapped in Google Universal Commerce Protocol
        format (title, extension field, no start/end dates). 1 merchant credit. Source: "ucp".
      tags: [Commerce Protocol Integration]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [merchantId]
              properties:
                merchantId:
                  type: string
                  description: "Merchant identifier"
                wallet:
                  type: string
                  description: "EVM wallet address (0x...)"
                solanaWallet:
                  type: string
                  description: "Solana wallet address"
                xrplWallet:
                  type: string
                  description: "XRPL wallet address (r-address)"
                items:
                  type: array
                  description: "Optional line items for per-item cent-amount allocations"
                  items:
                    type: object
                    properties:
                      path:
                        type: string
                        description: "JSONPath reference to the line item"
                        example: "$.line_items[0]"
                      amount:
                        type: integer
                        description: "Item price in cents"
                        example: 2500
      responses:
        "200":
          description: UCP-format discount response
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      protocol:
                        type: string
                        const: "ucp"
                      version:
                        type: string
                        example: "2026-01-11"
                      extension:
                        type: string
                        const: "dev.ucp.shopping.discount"
                      discounts:
                        type: object
                        properties:
                          codes:
                            type: array
                            items:
                              type: string
                          applied:
                            type: array
                            items:
                              type: object
                              properties:
                                code:
                                  type: string
                                title:
                                  type: string
                                amount:
                                  type: integer
                                  nullable: true
                                automatic:
                                  type: boolean
                                method:
                                  type: string
                                priority:
                                  type: integer
                                allocations:
                                  type: array
                                  items:
                                    type: object
                                    properties:
                                      path:
                                        type: string
                                      amount:
                                        type: integer
                      verification:
                        type: object
                        properties:
                          code:
                            type: string
                          expiresAt:
                            type: string
                            format: date-time
                          sig:
                            type: string
                          kid:
                            type: string
                  meta:
                    type: object
                    properties:
                      creditsRemaining:
                        type: integer
                      creditsCharged:
                        type: integer
                        description: "Always 1 for UCP discount checks"
                      version:
                        type: string
                      timestamp:
                        type: string
                        format: date-time
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: No merchant credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Merchant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: >
            Data source unavailable — retryable after a short delay.
            No discount signed, no credits charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RpcFailureEnvelope"

  /v1/codes/{code}:
    get:
      operationId: validateCode
      summary: Validate INSR-XXXXX discount code
      description: >
        Validates a discount code for merchant backends during ACP/UCP checkout flows.
        No authentication required. Does not expose wallet address, token breakdown, or analytics.
        Returns only what the merchant needs to apply the discount.
      tags: [Commerce Protocol Integration]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
            pattern: "^INSR-[A-Z0-9]{5}$"
          description: "Discount code in INSR-XXXXX format"
          example: "INSR-A7K3M"
      responses:
        "200":
          description: Code validation result
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  data:
                    oneOf:
                      - type: object
                        description: Valid code
                        properties:
                          valid:
                            type: boolean
                            const: true
                          code:
                            type: string
                          merchantId:
                            type: string
                          discountPercent:
                            type: integer
                          expiresAt:
                            type: string
                            format: date-time
                          createdAt:
                            type: string
                            format: date-time
                      - type: object
                        description: Invalid code
                        properties:
                          valid:
                            type: boolean
                            const: false
                          code:
                            type: string
                          reason:
                            type: string
                            enum: [expired, already_used, not_found]
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Invalid code format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

tags:
  - name: On-Chain Verification
    description: Privacy-preserving on-chain verification. Returns only boolean results, never balance amounts.
  - name: Discovery
    description: Browse merchants, tokens, and NFT collections in the public directory.
  - name: Discount Verification
    description: Calculate discounts, create verification codes, and confirm USDC payments.
  - name: Commerce Protocol Integration
    description: ACP (OpenAI/Stripe) and UCP (Google) compatible discount endpoints for AI agent commerce flows. Code validation for merchant backends.
  - name: Agent Onboarding
    description: Create and manage merchants programmatically. The API key that creates a merchant owns it.
