openapi: 3.0.3 info: title: SolaceScan API description: | SolaceScan public explorer API for Chain 138 with tiered access control. ## Authentication Track 1 endpoints are public and require no authentication. Track 2-4 endpoints require JWT authentication via wallet signature. ## Rate Limiting - Track 1: 100 requests/minute per IP - Track 2-4: Based on user tier and subscription version: 1.0.0 contact: name: API Support email: support@d-bis.org license: name: MIT url: https://opensource.org/licenses/MIT servers: - url: https://api.d-bis.org description: Production server - url: http://localhost:8080 description: Development server tags: - name: Health description: Health check endpoints - name: Auth description: Wallet and user-session authentication endpoints - name: Access description: RPC product catalog, subscriptions, and API key lifecycle - name: Blocks description: Block-related endpoints - name: Transactions description: Transaction-related endpoints - name: Addresses description: Address-related endpoints - name: Search description: Unified search endpoints - name: Track1 description: Public RPC gateway endpoints (no auth required) - name: MissionControl description: Public mission-control health, bridge trace, and cached liquidity helpers - name: Track2 description: Indexed explorer endpoints (auth required) - name: Track3 description: Analytics endpoints (Track 3+ required) - name: Track4 description: Operator endpoints (Track 4 + IP whitelist) paths: /health: get: tags: - Health summary: Health check description: Returns the health status of the API operationId: getHealth responses: '200': description: Service is healthy content: application/json: schema: type: object properties: status: type: string example: ok timestamp: type: string format: date-time database: type: string example: connected /api/v1/auth/nonce: post: tags: - Auth summary: Generate wallet auth nonce description: Creates a nonce challenge for wallet-signature authentication. operationId: createWalletAuthNonce requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WalletNonceRequest' responses: '200': description: Nonce generated content: application/json: schema: $ref: '#/components/schemas/WalletNonceResponse' '400': $ref: '#/components/responses/BadRequest' '503': description: Wallet auth storage or database not available /api/v1/auth/wallet: post: tags: - Auth summary: Authenticate with wallet signature description: Exchanges an address, signature, and nonce for a JWT used by wallet-authenticated track endpoints. operationId: authenticateWallet requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WalletAuthRequest' responses: '200': description: Wallet authenticated content: application/json: schema: $ref: '#/components/schemas/WalletAuthResponse' '401': $ref: '#/components/responses/Unauthorized' '503': description: Wallet auth storage or database not available /api/v1/auth/register: post: tags: - Auth summary: Register an explorer access user description: "Creates an email/password account for the `/access` console and returns a user session token." operationId: registerAccessUser requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserRegisterRequest' responses: '200': description: User created and session issued content: application/json: schema: $ref: '#/components/schemas/UserSessionResponse' '400': $ref: '#/components/responses/BadRequest' '503': description: Database not available /api/v1/auth/login: post: tags: - Auth summary: Log in to the explorer access console description: "Authenticates an email/password user and returns a user session token for `/api/v1/access/*` endpoints." operationId: loginAccessUser requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserLoginRequest' responses: '200': description: Session issued content: application/json: schema: $ref: '#/components/schemas/UserSessionResponse' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/access/me: get: tags: - Access summary: Get current access-console user description: Returns the signed-in user profile and any known product subscriptions. operationId: getAccessMe security: - userSessionAuth: [] responses: '200': description: Current user and subscriptions content: application/json: schema: $ref: '#/components/schemas/AccessMeResponse' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/access/products: get: tags: - Access summary: List available RPC access products description: Returns the commercial and operational RPC products currently modeled by the explorer access layer. operationId: listAccessProducts responses: '200': description: Product catalog content: application/json: schema: $ref: '#/components/schemas/AccessProductsResponse' /api/v1/access/subscriptions: get: tags: - Access summary: List subscriptions for the signed-in user operationId: listAccessSubscriptions security: - userSessionAuth: [] responses: '200': description: Subscription list content: application/json: schema: $ref: '#/components/schemas/AccessSubscriptionsResponse' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/access/admin/subscriptions: get: tags: - Access summary: List subscriptions for admin review description: Returns pending or filtered subscriptions for users whose email is allowlisted in `ACCESS_ADMIN_EMAILS`. operationId: listAccessAdminSubscriptions security: - userSessionAuth: [] parameters: - name: status in: query required: false schema: type: string enum: [pending, active, suspended, revoked] responses: '200': description: Subscription list content: application/json: schema: $ref: '#/components/schemas/AccessSubscriptionsResponse' '401': $ref: '#/components/responses/Unauthorized' '403': description: Admin privileges required '503': description: Database not available post: tags: - Access summary: Approve, suspend, or revoke a subscription operationId: updateAccessAdminSubscription security: - userSessionAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AdminSubscriptionActionRequest' responses: '200': description: Subscription updated content: application/json: schema: $ref: '#/components/schemas/AccessSubscriptionResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': description: Admin privileges required '503': description: Database not available post: tags: - Access summary: Request or activate product access description: | Creates or updates a product subscription. Self-service products become `active` immediately. Approval-gated products such as Core RPC are created in `pending` state. operationId: createAccessSubscription security: - userSessionAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateSubscriptionRequest' responses: '200': description: Subscription saved content: application/json: schema: $ref: '#/components/schemas/AccessSubscriptionResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/access/api-keys: get: tags: - Access summary: List API keys for the signed-in user operationId: listAccessApiKeys security: - userSessionAuth: [] responses: '200': description: API key records content: application/json: schema: $ref: '#/components/schemas/AccessAPIKeysResponse' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available post: tags: - Access summary: Create an API key description: | Issues an API key for the chosen tier and product. If the product is approval-gated and not already active for the user, this endpoint returns `subscription_required`. operationId: createAccessApiKey security: - userSessionAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAPIKeyRequest' responses: '200': description: API key created content: application/json: schema: $ref: '#/components/schemas/CreateAPIKeyResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': description: Product access is pending approval or inactive content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: subscription_required message: Product access is pending approval or inactive '503': description: Database not available /api/v1/access/api-keys/{id}: post: tags: - Access summary: Revoke an API key description: "Revokes the identified API key. `DELETE` is also accepted by the handler, but the current frontend uses `POST`." operationId: revokeAccessApiKey security: - userSessionAuth: [] parameters: - name: id in: path required: true schema: type: string responses: '200': description: API key revoked content: application/json: schema: $ref: '#/components/schemas/RevokeAPIKeyResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available delete: tags: - Access summary: Revoke an API key description: Alternate HTTP verb for API key revocation. operationId: revokeAccessApiKeyDelete security: - userSessionAuth: [] parameters: - name: id in: path required: true schema: type: string responses: '200': description: API key revoked content: application/json: schema: $ref: '#/components/schemas/RevokeAPIKeyResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/access/usage: get: tags: - Access summary: Get usage summary for the signed-in user description: Returns aggregated per-product usage derived from issued API keys. operationId: getAccessUsage security: - userSessionAuth: [] responses: '200': description: Usage summary content: application/json: schema: $ref: '#/components/schemas/AccessUsageResponse' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/access/audit: get: tags: - Access summary: Get recent API activity for the signed-in user description: Returns recent validated API-key usage log rows for the current user. operationId: getAccessAudit security: - userSessionAuth: [] parameters: - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 200 default: 20 responses: '200': description: Audit entries content: application/json: schema: $ref: '#/components/schemas/AccessAuditResponse' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/access/admin/audit: get: tags: - Access summary: Get recent API activity across users for admin review description: Returns recent validated API-key usage log rows for access admins, optionally filtered by product. operationId: getAccessAdminAudit security: - userSessionAuth: [] parameters: - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 500 default: 50 - name: product in: query required: false schema: type: string responses: '200': description: Audit entries content: application/json: schema: $ref: '#/components/schemas/AccessAuditResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '503': description: Database not available /api/v1/access/internal/validate-key: get: tags: - Access summary: Validate an API key for nginx auth_request or similar edge subrequests description: >- Requires `X-Access-Internal-Secret` and accepts the presented API key in `X-API-Key` or `Authorization: Bearer ...`. Returns `200` or `401` and emits validation metadata in response headers. operationId: validateAccessApiKeyInternalGet parameters: - name: X-Access-Internal-Secret in: header required: true schema: type: string - name: X-API-Key in: header required: false schema: type: string - name: Authorization in: header required: false schema: type: string - name: X-Access-Method in: header required: false schema: type: string - name: X-Access-Request-Count in: header required: false schema: type: integer responses: '200': description: Key validated headers: X-Validated-Product: schema: type: string X-Validated-Tier: schema: type: string X-Validated-Scopes: schema: type: string X-Quota-Remaining: schema: type: string '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available post: tags: - Access summary: Validate an API key for internal edge enforcement description: Requires `X-Access-Internal-Secret` and returns validated key metadata while incrementing usage counters. operationId: validateAccessApiKeyInternal parameters: - name: X-Access-Internal-Secret in: header required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/InternalValidateAPIKeyRequest' responses: '200': description: Key validated content: application/json: schema: $ref: '#/components/schemas/InternalValidateAPIKeyResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '503': description: Database not available /api/v1/blocks: get: tags: - Blocks summary: List blocks description: Returns a paginated list of blocks operationId: listBlocks parameters: - name: limit in: query description: Number of blocks to return required: false schema: type: integer minimum: 1 maximum: 100 default: 20 - name: page in: query description: Page number required: false schema: type: integer minimum: 1 default: 1 - name: chain_id in: query description: Chain ID filter required: false schema: type: integer default: 138 responses: '200': description: List of blocks content: application/json: schema: $ref: '#/components/schemas/BlockListResponse' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' /api/v1/blocks/{chain_id}/{number}: get: tags: - Blocks summary: Get block by number description: Returns block details by chain ID and block number operationId: getBlockByNumber parameters: - name: chain_id in: path required: true description: Chain ID schema: type: integer example: 138 - name: number in: path required: true description: Block number schema: type: integer example: 1000 responses: '200': description: Block details content: application/json: schema: $ref: '#/components/schemas/Block' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/transactions: get: tags: - Transactions summary: List transactions description: Returns a paginated list of transactions operationId: listTransactions parameters: - name: limit in: query schema: type: integer default: 20 - name: page in: query schema: type: integer default: 1 - name: chain_id in: query schema: type: integer default: 138 responses: '200': description: List of transactions content: application/json: schema: $ref: '#/components/schemas/TransactionListResponse' /api/v1/search: get: tags: - Search summary: Unified search description: | Searches for blocks, transactions, or addresses. Automatically detects the type based on the query format. operationId: search parameters: - name: q in: query required: true description: Search query (block number, address, or transaction hash) schema: type: string example: "0x1234567890abcdef" responses: '200': description: Search results content: application/json: schema: $ref: '#/components/schemas/SearchResponse' '400': $ref: '#/components/responses/BadRequest' /api/v1/track1/blocks/latest: get: tags: - Track1 summary: Get latest blocks (Public) description: Returns the latest blocks via RPC gateway. No authentication required. operationId: getLatestBlocks parameters: - name: limit in: query schema: type: integer default: 10 maximum: 50 responses: '200': description: Latest blocks content: application/json: schema: $ref: '#/components/schemas/BlockListResponse' /api/v1/mission-control/stream: get: tags: - MissionControl summary: Mission-control SSE stream description: | Server-Sent Events stream with the same inner `data` payload as `GET /api/v1/track1/bridge/status`. Emits one event immediately, then refreshes every 20 seconds. Configure nginx with `proxy_buffering off`. operationId: getMissionControlStream responses: '200': description: SSE stream content: text/event-stream: schema: type: string /api/v1/mission-control/liquidity/token/{address}/pools: get: tags: - MissionControl summary: Cached liquidity proxy description: | 30-second in-memory cached proxy to the token-aggregation pools endpoint for the configured `CHAIN_ID`. operationId: getMissionControlLiquidityPools parameters: - name: address in: path required: true schema: type: string pattern: '^0x[a-fA-F0-9]{40}$' responses: '200': description: Upstream JSON response '400': $ref: '#/components/responses/BadRequest' '503': description: "`TOKEN_AGGREGATION_BASE_URL` not configured" /api/v1/mission-control/bridge/trace: get: tags: - MissionControl summary: Resolve a transaction through Blockscout and label 138-side contracts description: | Queries Blockscout using `BLOCKSCOUT_INTERNAL_URL` and labels the `from` and `to` addresses using Chain 138 entries from `SMART_CONTRACTS_MASTER_JSON`. operationId: getMissionControlBridgeTrace parameters: - name: tx in: query required: true schema: type: string pattern: '^0x[a-fA-F0-9]{64}$' responses: '200': description: Labeled bridge trace '400': $ref: '#/components/responses/BadRequest' '502': description: Blockscout lookup failed /api/v1/track4/operator/run-script: post: tags: - Track4 summary: Run an allowlisted operator script description: | Track 4 endpoint. Requires authenticated wallet, IP allowlisting, `OPERATOR_SCRIPTS_ROOT`, and `OPERATOR_SCRIPT_ALLOWLIST`. operationId: runOperatorScript security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [script] properties: script: type: string description: "Path relative to `OPERATOR_SCRIPTS_ROOT`" args: type: array items: type: string maxItems: 24 responses: '200': description: Script execution result '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '503': description: Script root or allowlist not configured /api/v1/track2/search: get: tags: - Track2 summary: Advanced search (Auth Required) description: Advanced search with indexed data. Requires Track 2+ authentication. operationId: track2Search security: - bearerAuth: [] parameters: - name: q in: query required: true schema: type: string responses: '200': description: Search results '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: JWT token obtained from /api/v1/auth/wallet userSessionAuth: type: http scheme: bearer bearerFormat: JWT description: User session token obtained from /api/v1/auth/register or /api/v1/auth/login schemas: WalletNonceRequest: type: object required: [address] properties: address: type: string pattern: '^0x[a-fA-F0-9]{40}$' WalletNonceResponse: type: object properties: address: type: string nonce: type: string message: type: string WalletAuthRequest: type: object required: [address, signature, nonce] properties: address: type: string pattern: '^0x[a-fA-F0-9]{40}$' signature: type: string nonce: type: string WalletAuthResponse: type: object properties: token: type: string expires_at: type: string format: date-time user: type: object additionalProperties: true User: type: object properties: id: type: string email: type: string format: email username: type: string is_admin: type: boolean UserRegisterRequest: type: object required: [email, username, password] properties: email: type: string format: email username: type: string password: type: string minLength: 8 UserLoginRequest: type: object required: [email, password] properties: email: type: string format: email password: type: string UserSessionResponse: type: object properties: user: $ref: '#/components/schemas/User' token: type: string expires_at: type: string format: date-time AccessProduct: type: object properties: slug: type: string name: type: string provider: type: string vmid: type: integer http_url: type: string ws_url: type: string default_tier: type: string requires_approval: type: boolean billing_model: type: string description: type: string use_cases: type: array items: type: string management_features: type: array items: type: string AccessProductsResponse: type: object properties: products: type: array items: $ref: '#/components/schemas/AccessProduct' note: type: string AccessAPIKeyRecord: type: object properties: id: type: string name: type: string tier: type: string productSlug: type: string scopes: type: array items: type: string monthlyQuota: type: integer requestsUsed: type: integer approved: type: boolean approvedAt: type: string format: date-time nullable: true rateLimitPerSecond: type: integer rateLimitPerMinute: type: integer lastUsedAt: type: string format: date-time nullable: true expiresAt: type: string format: date-time nullable: true revoked: type: boolean createdAt: type: string format: date-time AccessSubscription: type: object properties: id: type: string productSlug: type: string tier: type: string status: type: string enum: [active, pending, suspended, revoked] monthlyQuota: type: integer requestsUsed: type: integer requiresApproval: type: boolean approvedAt: type: string format: date-time nullable: true approvedBy: type: string nullable: true notes: type: string nullable: true createdAt: type: string format: date-time AccessUsageSummary: type: object properties: product_slug: type: string active_keys: type: integer requests_used: type: integer monthly_quota: type: integer AccessMeResponse: type: object properties: user: $ref: '#/components/schemas/User' subscriptions: type: array items: $ref: '#/components/schemas/AccessSubscription' AccessSubscriptionsResponse: type: object properties: subscriptions: type: array items: $ref: '#/components/schemas/AccessSubscription' AccessSubscriptionResponse: type: object properties: subscription: $ref: '#/components/schemas/AccessSubscription' AccessAPIKeysResponse: type: object properties: api_keys: type: array items: $ref: '#/components/schemas/AccessAPIKeyRecord' CreateSubscriptionRequest: type: object required: [product_slug] properties: product_slug: type: string tier: type: string CreateAPIKeyRequest: type: object required: [name] properties: name: type: string tier: type: string product_slug: type: string expires_days: type: integer monthly_quota: type: integer scopes: type: array items: type: string AdminSubscriptionActionRequest: type: object required: [subscription_id, status] properties: subscription_id: type: string status: type: string enum: [active, suspended, revoked] notes: type: string CreateAPIKeyResponse: type: object properties: api_key: type: string description: Plaintext key is only returned at creation time. record: $ref: '#/components/schemas/AccessAPIKeyRecord' RevokeAPIKeyResponse: type: object properties: revoked: type: boolean api_key_id: type: string AccessUsageResponse: type: object properties: usage: type: array items: $ref: '#/components/schemas/AccessUsageSummary' AccessAuditEntry: type: object properties: id: type: integer apiKeyId: type: string keyName: type: string productSlug: type: string methodName: type: string requestCount: type: integer lastIp: type: string nullable: true createdAt: type: string format: date-time AccessAuditResponse: type: object properties: entries: type: array items: $ref: '#/components/schemas/AccessAuditEntry' InternalValidatedAPIKey: type: object properties: apiKeyId: type: string userId: type: string name: type: string tier: type: string productSlug: type: string scopes: type: array items: type: string monthlyQuota: type: integer requestsUsed: type: integer rateLimitPerSecond: type: integer rateLimitPerMinute: type: integer lastUsedAt: type: string format: date-time nullable: true expiresAt: type: string format: date-time nullable: true InternalValidateAPIKeyRequest: type: object required: [api_key] properties: api_key: type: string method_name: type: string request_count: type: integer last_ip: type: string InternalValidateAPIKeyResponse: type: object properties: valid: type: boolean key: $ref: '#/components/schemas/InternalValidatedAPIKey' Block: type: object properties: chain_id: type: integer example: 138 number: type: integer example: 1000 hash: type: string example: "0x1234567890abcdef" parent_hash: type: string timestamp: type: string format: date-time miner: type: string transaction_count: type: integer gas_used: type: integer gas_limit: type: integer Transaction: type: object properties: chain_id: type: integer hash: type: string block_number: type: integer from_address: type: string to_address: type: string value: type: string gas: type: integer gas_price: type: string status: type: string enum: [success, failed] BlockListResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/Block' pagination: $ref: '#/components/schemas/Pagination' TransactionListResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/Transaction' pagination: $ref: '#/components/schemas/Pagination' Pagination: type: object properties: page: type: integer limit: type: integer total: type: integer total_pages: type: integer SearchResponse: type: object properties: query: type: string results: type: array items: type: object properties: type: type: string enum: [block, transaction, address] data: type: object Error: type: object properties: error: type: object properties: code: type: string message: type: string responses: BadRequest: description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: "bad_request" message: "Invalid request parameters" Unauthorized: description: Unauthorized - Authentication required content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: "unauthorized" message: "Authentication required" Forbidden: description: Forbidden - Insufficient permissions content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: "forbidden" message: "Insufficient permissions. Track 2+ required." NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: "not_found" message: "Resource not found" InternalServerError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: "internal_error" message: "An internal error occurred"