{
  "info": {
    "_postman_id": "onchainrisk-sandbox-v1",
    "name": "OnChainRisk Sandbox API",
    "description": "Public sandbox tier — integration testing against real OnChainRisk API.\n\n**Setup (one-time, via dashboard — NOT via this collection):**\n1. Sign up at https://onchainrisk.io/register (CAPTCHA required)\n2. Dashboard → API Keys → Create sandbox key (1 max for free tier)\n3. Copy the key (shown once)\n4. In Postman: set env var `sandbox_api_key` to your key\n\nAll requests here use that key via `Authorization: Bearer {{sandbox_api_key}}`. Key management (create/rotate/revoke) lives in the dashboard — not exposed to sandbox because the self-service flow is intentional.\n\n**Sandbox gates expensive fields** — responses match paid shape but `crossChain`, `tokenFlow`, `clusters`, `honeypot`, etc. are always `null`/`[]`. See `_sandbox.gated_fields[]` in every response for the authoritative list.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.onchainrisk.io",
      "type": "string"
    },
    {
      "key": "sandbox_api_key",
      "value": "ocr_test_REPLACE_ME",
      "type": "string",
      "description": "Your sandbox API key. New sandbox keys use the ocr_test_ prefix; legacy sandbox keys may still carry ocr_live_ and continue to authenticate as sandbox. Create one in the dashboard: https://onchainrisk.io/dashboard/api-keys"
    },
    {
      "key": "sample_address",
      "value": "0xa39b189482f984388a34460636fea9eb181ad1a6",
      "type": "string",
      "description": "Light demo address — runs fast. Swap to your own for real integration testing."
    }
  ],
  "item": [
    {
      "name": "1. Check address — POST /api/v1/check",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{sandbox_api_key}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"address\": \"{{sample_address}}\",\n  \"network\": \"eth\"\n}"
        },
        "url": {
          "raw": "{{base_url}}/api/v1/check",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "api",
            "v1",
            "check"
          ]
        },
        "description": "Sandbox risk analysis for one address. The response carries a `_sandbox` block and gates the paid-only sections (`crossChain`, `tokenFlow`, `clusters`) to null. Validation failures return 422 with `error.code = VALIDATION_ERROR` — the stored examples below show each granular `details.errorCode`."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "pm.test('_sandbox block present', () => pm.expect(d._sandbox).to.be.an('object'));",
              "pm.test('_sandbox.profile = sandbox', () => pm.expect(d._sandbox.profile).to.equal('sandbox'));",
              "pm.test('_sandbox.reports_saved = false', () => pm.expect(d._sandbox.reports_saved).to.equal(false));",
              "pm.test('gated: crossChain is null', () => pm.expect(d.crossChain).to.equal(null));",
              "pm.test('gated: tokenFlow is null', () => pm.expect(d.tokenFlow).to.equal(null));",
              "pm.test('gated: clusters is null', () => pm.expect(d.clusters).to.equal(null));",
              "pm.test('gated: topCounterparties is []', () => pm.expect(d.topCounterparties).to.eql([]));",
              "pm.test('no reportId (sandbox does not persist)', () => pm.expect(d).to.not.have.property('reportId'));",
              "pm.test('no analysisMode (omitted by policy)', () => pm.expect(d).to.not.have.property('analysisMode'));"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// Stable validation envelope: validation errors always 422 with",
              "// success:false + error.code=VALIDATION_ERROR + details.errorCode.",
              "// Added 2026-04-24 after the Tron/network mismatch incident.",
              "// (Ref: docs/testing/fixtures/validation/*.json — same shape.)",
              "pm.test('validation envelope is stable (422)', function () {",
              "    if (pm.response.code !== 422) return;",
              "    const body = pm.response.json();",
              "    pm.expect(body).to.have.property('success', false);",
              "    pm.expect(body.error).to.have.property('code', 'VALIDATION_ERROR');",
              "    pm.expect(body.error).to.have.property('message');",
              "    pm.expect(body.error).to.have.property('details');",
              "    pm.expect(body.error.details).to.have.property('errorCode');",
              "});"
            ]
          }
        }
      ],
      "response": [
        {
          "name": "422 ADDRESS_NETWORK_MISMATCH (Tron + network=eth)",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{sandbox_api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/check",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "check"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"address\": \"TLA2W3khvaLjZ7FQ3cwh1zHszVRV627cr5\",\n  \"network\": \"eth\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Unprocessable Entity",
          "code": 422,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"success\": false,\n  \"error\": {\n    \"message\": \"Validation failed\",\n    \"code\": \"VALIDATION_ERROR\",\n    \"details\": {\n      \"address\": \"Address format does not match selected network. The address looks like a tron address, but network was set to eth.\",\n      \"errorCode\": \"ADDRESS_NETWORK_MISMATCH\",\n      \"detectedFamily\": \"tron\",\n      \"requestedNetwork\": \"eth\"\n    }\n  }\n}"
        },
        {
          "name": "422 INVALID_ADDRESS (whitespace-only)",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{sandbox_api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/check",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "check"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"address\": \"   \",\n  \"network\": \"eth\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Unprocessable Entity",
          "code": 422,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"success\": false,\n  \"error\": {\n    \"message\": \"Validation failed\",\n    \"code\": \"VALIDATION_ERROR\",\n    \"details\": {\n      \"address\": \"Address is required.\",\n      \"errorCode\": \"INVALID_ADDRESS\"\n    }\n  }\n}"
        },
        {
          "name": "422 UNSUPPORTED_NETWORK (network=Ethereum)",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{sandbox_api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/check",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "check"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"address\": \"0x742d35Cc6634C0532925a3b844Bc454e4438f44e\",\n  \"network\": \"Ethereum\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Unprocessable Entity",
          "code": 422,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"success\": false,\n  \"error\": {\n    \"message\": \"Validation failed\",\n    \"code\": \"VALIDATION_ERROR\",\n    \"details\": {\n      \"address\": \"Unsupported network 'ethereum'. Supported: arbitrum, avalanche, base, bsc, btc, cardano, celo, cosmos, cronos, eth, fantom, linea, ltc, moonbeam, optimism, polygon, polygon_zkevm, scroll, sol, ton, tron, xrp, zksync.\",\n      \"errorCode\": \"UNSUPPORTED_NETWORK\",\n      \"requestedNetwork\": \"ethereum\"\n    }\n  }\n}"
        },
        {
          "name": "422 INVALID_ADDRESS_CHECKSUM (Tron bad Base58Check)",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{sandbox_api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/check",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "check"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"address\": \"TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT\",\n  \"network\": \"tron\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Unprocessable Entity",
          "code": 422,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"success\": false,\n  \"error\": {\n    \"message\": \"Validation failed\",\n    \"code\": \"VALIDATION_ERROR\",\n    \"details\": {\n      \"address\": \"Address failed tron checksum validation.\",\n      \"errorCode\": \"INVALID_ADDRESS_CHECKSUM\",\n      \"detectedFamily\": \"tron\",\n      \"requestedNetwork\": \"tron\"\n    }\n  }\n}"
        },
        {
          "name": "422 COULD_NOT_DETECT_NETWORK (unrecognisable address)",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{sandbox_api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/check",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "check"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"address\": \"not_an_address_at_all_12345\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Unprocessable Entity",
          "code": 422,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"success\": false,\n  \"error\": {\n    \"message\": \"Validation failed\",\n    \"code\": \"VALIDATION_ERROR\",\n    \"details\": {\n      \"address\": \"Could not detect network from address format. Please specify the network parameter.\",\n      \"errorCode\": \"COULD_NOT_DETECT_NETWORK\"\n    }\n  }\n}"
        }
      ]
    },
    {
      "name": "2. Graph preview — POST /api/v1/sandbox/graph/expand",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{sandbox_api_key}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"address\": \"{{sample_address}}\",\n  \"network\": \"eth\",\n  \"depth\": 1\n}"
        },
        "url": {
          "raw": "{{base_url}}/api/v1/sandbox/graph/expand",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "api",
            "v1",
            "sandbox",
            "graph",
            "expand"
          ]
        },
        "description": "Cheap sandbox graph preview. Depth 1, last 7 days, ≤20 nodes, ≤30 edges. Aggregated counterparty edges in the same shape as paid /api/graph/expand. No report persistence, no usage row, no credit charge. Upgrade for full historical graph expansion."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "pm.test('nodes is array', () => pm.expect(d.nodes).to.be.an('array'));",
              "pm.test('edges is array', () => pm.expect(d.edges).to.be.an('array'));",
              "pm.test('nodes <= 20', () => pm.expect(d.nodes.length).to.be.at.most(20));",
              "pm.test('edges <= 30', () => pm.expect(d.edges.length).to.be.at.most(30));",
              "pm.test('_sandbox.profile = sandbox', () => pm.expect(d._sandbox.profile).to.equal('sandbox'));",
              "pm.test('_sandbox.coverage_days = 7', () => pm.expect(d._sandbox.coverage_days).to.equal(7));",
              "pm.test('_sandbox.limits.depth = 1', () => pm.expect(d._sandbox.limits.depth).to.equal(1));",
              "pm.test('_sandbox.limits.max_nodes = 20', () => pm.expect(d._sandbox.limits.max_nodes).to.equal(20));",
              "pm.test('_sandbox.limits.max_edges = 30', () => pm.expect(d._sandbox.limits.max_edges).to.equal(30));",
              "pm.test('_sandbox.gated_fields includes clusters', () => pm.expect(d._sandbox.gated_fields).to.include('clusters'));",
              "pm.test('reports_saved = false', () => pm.expect(d._sandbox.reports_saved).to.equal(false));"
            ]
          }
        }
      ]
    },
    {
      "name": "3. Quota — GET /api/v1/usage",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{sandbox_api_key}}"
          }
        ],
        "url": {
          "raw": "{{base_url}}/api/v1/usage",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "api",
            "v1",
            "usage"
          ]
        },
        "description": "Quota and rate-limit introspection. Sandbox keys are accepted here and report `key_type: \"sandbox\"`. This is the supported public replacement for the internal account endpoints — no profile or account data is returned."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "pm.test('key_type = sandbox', () => pm.expect(d.key_type).to.equal('sandbox'));",
              "pm.test('plan_quota present', () => pm.expect(d.plan_quota).to.be.an('object'));",
              "pm.test('rate_limit.requests_per_second is a number', () => pm.expect(d.rate_limit.requests_per_second).to.be.a('number'));",
              "pm.test('no account/profile fields leak in', () => {",
              "    pm.expect(d).to.not.have.property('email');",
              "    pm.expect(d).to.not.have.property('id');",
              "});"
            ]
          }
        }
      ]
    }
  ]
}
