{
  "schema": "nexa-balance/1",
  "site": {
    "id": "nexa-ecosystem",
    "name": "NEXA ECOSYSTEM",
    "origin": "https://www.nexa-ecosystem.com"
  },
  "summary": "Public discovery is free. Protected concepts and their images cost BALANCE (see model): create a wallet + API token, buy an EUR package — an agent with a Shared Payment Token pays it itself over MPP, otherwise Stripe Checkout — and repeat the request with Authorization: Bearer. No account, no contact, no cookies needed.",
  "network": {
    "name": "NEXA",
    "shared_balance": true,
    "sites": [
      {
        "id": "tactical-orbit",
        "name": "TACTICAL ORBIT",
        "origin": "https://tactical-orbit.com"
      },
      {
        "id": "nexa-smartlife",
        "name": "NEXA SMARTLIFE",
        "origin": "https://nexa-smartlife.com"
      },
      {
        "id": "nexa-ecosystem",
        "name": "NEXA ECOSYSTEM",
        "origin": "https://nexa-ecosystem.com"
      }
    ],
    "statement": "One BALANCE for all NEXA sites. Buy once and use your BALANCE across all three sites."
  },
  "model": {
    "unit": "BALANCE",
    "product_view_price": 6,
    "window_size": 4,
    "window_inactivity_hours": 24,
    "lot_validity_days": 30,
    "payment_currency": "eur",
    "rules": [
      "A product already in your WINDOW costs 0 BALANCE.",
      "Any other product view costs 6 BALANCE and enters the WINDOW (strict FIFO, the oldest beyond 4 is locked).",
      "On each site the two concepts at positions 1 and 2 are free for everybody, every time (no wallet, no BALANCE, no WINDOW place); they can change at any time.",
      "Protected media is served only while its product is in the WINDOW.",
      "Each purchase is its own lot, valid from its own payment; the soonest-expiring lot is spent first."
    ]
  },
  "packages": [
    {
      "package": "balance-720",
      "price": {
        "currency": "eur",
        "amount": 720
      },
      "balance": 720,
      "valid_days": 30
    },
    {
      "package": "balance-1020",
      "price": {
        "currency": "eur",
        "amount": 1020
      },
      "balance": 1020,
      "valid_days": 30
    },
    {
      "package": "balance-2520",
      "price": {
        "currency": "eur",
        "amount": 2520
      },
      "balance": 2520,
      "valid_days": 30
    }
  ],
  "payment_options": [
    {
      "rail": "mpp",
      "protocol": "mpp",
      "method": "stripe",
      "intent": "charge",
      "payer_credential": "shared_payment_token",
      "for": "AI agents with a supported payment capability (a Stripe Shared Payment Token, e.g. from Link Agent Wallet)",
      "start": {
        "method": "POST",
        "href": "https://www.nexa-ecosystem.com/api/balance/checkout",
        "headers": {
          "Authorization": "Bearer <NEXA BALANCE API token>",
          "Content-Type": "application/json"
        },
        "body": {
          "package": "<package id from packages[]>",
          "rail": "mpp"
        }
      },
      "returns": "pay_url (NEXA BALANCE) + purchase",
      "pay": "POST pay_url → 402 with WWW-Authenticate: Payment method=\"stripe\" intent=\"charge\" → repeat with Authorization: Payment <credential carrying your Shared Payment Token>",
      "example": "link-cli mpp pay <pay_url> --method POST --context \"<what you buy and why, at least 100 characters>\"",
      "confirmation": "200 + Payment-Receipt (the Stripe PaymentIntent); the verified Stripe webhook adds the BALANCE on its own — request the concept again; only if it still answers 402, check steps.check_payment ONCE later. Never poll.",
      "step": "pay_mpp",
      "discovery": "https://balance.nexa-ecosystem.com/openapi.json"
    },
    {
      "rail": "checkout",
      "protocol": "stripe_checkout",
      "for": "people and operators in a browser (fallback)",
      "start": {
        "method": "POST",
        "href": "https://www.nexa-ecosystem.com/api/balance/checkout",
        "headers": {
          "Authorization": "Bearer <NEXA BALANCE API token>",
          "Content-Type": "application/json"
        },
        "body": {
          "package": "<package id from packages[]>"
        }
      },
      "returns": "checkout_url + purchase + status AWAITING_PAYMENT",
      "pay": "a person (the operator) or a system authorised to pay opens checkout_url in a browser and pays (EUR, one-time; the URL is valid 24 hours)",
      "confirmation": "Your request ends with status AWAITING_PAYMENT — do not wait and do not poll. The verified Stripe webhook adds the BALANCE on its own after the payment; when you resume work later, request the concept again (or check steps.check_payment ONCE).",
      "step": "pay"
    }
  ],
  "principle": {
    "public_discovery": "Free for people, search engines and crawlers.",
    "free_positions": "The two concepts at positions 1 and 2 (access \"free\" in the catalog) are free for everybody, every time — their full description and images too, without a token.",
    "protected_content": "The same NEXA BALANCE model for every client: person, AI agent or crawler.",
    "user_agent": "The User-Agent never grants access to protected content; only a credential does."
  },
  "public_discovery": {
    "catalog": {
      "method": "GET",
      "href": "https://www.nexa-ecosystem.com/api/catalog/concepts",
      "cost": 0,
      "change_detection": "version, content_hash per concept; ETag / If-None-Match on the list (free)"
    },
    "covers": "cover.url — public only for the concepts at positions 1 and 2 (null for every other concept)"
  },
  "protected": {
    "concept": {
      "method": "GET",
      "href": "https://www.nexa-ecosystem.com/api/catalog/concepts/{content_id}",
      "cost": "one PRODUCT VIEW (see rules)",
      "credential": "Authorization: Bearer <NEXA BALANCE API token>",
      "idempotency": "Idempotency-Key header (recommended)",
      "returns": "the public fields + concept_description + gallery[] (media_url per image) + access { charged, balance, window, locked }"
    },
    "media": {
      "method": "GET",
      "href": "https://www.nexa-ecosystem.com/api/catalog/media/{asset_id}",
      "cost": 0,
      "condition": "only while the concept is in your WINDOW — fetch images right after the concept"
    },
    "per_credential_state": {
      "method": "GET",
      "href": "https://www.nexa-ecosystem.com/api/balance/state",
      "cost": 0,
      "returns": "balance, window, free_positions, and per concept: status (free for positions 1 / 2)"
    }
  },
  "rules": [
    "FREE: the two concepts at positions 1 and 2 of this site (access \"free\", listed first in the catalog; free_positions) are free for everybody, every time — no token, no BALANCE, no WINDOW place. The owner can change them at any time; a concept that leaves position 1 / 2 costs model.product_view_price again.",
    "Opening any other concept is one PRODUCT VIEW: 0 BALANCE when it is already in your WINDOW, otherwise model.product_view_price.",
    "If you open a concept because the catalog showed it as free, send \"X-NEXA-Expect-Free: true\": should it no longer be free, you get 409 status_changed and nothing is charged.",
    "Your WINDOW holds the last model.window_size concepts you opened with this token (first in, first out) and closes model.window_inactivity_hours hours after your last concept request. Images are served only while their concept is in it.",
    "A conditional request (If-None-Match) to a concept is also a PRODUCT VIEW — decide from the free catalog's version / content_hash whether you need to open it at all.",
    "Send an Idempotency-Key with every concept request. Repeating a request with the same key never charges twice and returns the first answer (for 60 seconds) — so reuse a key only when you did not get the answer (timeout, 503), and use a NEW key for a new attempt, e.g. after a 402 or after paying.",
    "BALANCE belongs to the wallet: all its tokens and all NEXA sites (network.sites) spend the same BALANCE. Each token has its own WINDOW.",
    "Packages are paid in EUR (one-time). Each purchase is valid valid_days days from its payment; the soonest-expiring purchase is spent first.",
    "The User-Agent never changes access or price. Cookies are not used for machine access.",
    "Paying a package: with rail \"mpp\" an agent pays itself with a Stripe Shared Payment Token (no browser); Stripe Checkout is the fallback for people and operators. Either way BALANCE is credited only by NEXA BALANCE after the verified Stripe webhook, independently of you. One request, one answer: never poll and never keep a connection open — when you resume work, open the concept again with a NEW Idempotency-Key (or check check_payment once).",
    "A purchase is paid at most once: repeating a payment request never charges twice (409 already-paid / payment-in-progress, 503 outcome-unknown with Retry-After)."
  ],
  "flow": [
    {
      "step": "discover",
      "do": "GET public_discovery.catalog (free, no credential). Pick concepts by content_id; compare version / content_hash with what you already have.",
      "next": "open"
    },
    {
      "step": "open",
      "do": "GET protected.concept with Authorization: Bearer and an Idempotency-Key.",
      "on": {
        "200": {
          "next": "media"
        },
        "401": {
          "next": "get_wallet",
          "or": "recover_token",
          "note": "the response field next_step says which one (no token → get_wallet; token rejected → recover_token)"
        },
        "402": {
          "next": "choose_package"
        },
        "404": {
          "next": "discover",
          "note": "unknown content_id"
        },
        "409": {
          "next": "open",
          "note": "idempotency_key_reused: that Idempotency-Key belongs to another request — use a new key; status_changed (with X-NEXA-Expect-Free): no longer free, nothing charged — decide again"
        },
        "503": {
          "next": "open",
          "note": "wait, then repeat with the SAME Idempotency-Key"
        }
      }
    },
    {
      "step": "get_wallet",
      "do": "POST steps.get_wallet; keep balance_code private, use token as Bearer.",
      "next": "open"
    },
    {
      "step": "choose_package",
      "do": "Pick a package from packages[] that covers \"required\" (steps.choose_package.suggested is the cheapest).",
      "next": "buy"
    },
    {
      "step": "buy",
      "do": "POST steps.buy with { package, resume, rail } — rail \"mpp\" if you can pay with a Shared Payment Token (see payment_options), otherwise Stripe Checkout.",
      "on": {
        "mpp": {
          "next": "pay_mpp"
        },
        "checkout": {
          "next": "pay"
        }
      }
    },
    {
      "step": "pay_mpp",
      "do": "Answer the 402 of pay_url with an SPT credential (e.g. link-cli mpp pay <pay_url>); 200 + Payment-Receipt means paid. Then open the concept again (the webhook credits within seconds).",
      "next": "open"
    },
    {
      "step": "pay",
      "do": "checkout_url (valid 24 hours) is paid by a person (operator) or a system authorised to pay, in a browser. Your request is finished: status AWAITING_PAYMENT. Do not wait for the payment and do not poll.",
      "next": "check_payment"
    },
    {
      "step": "check_payment",
      "do": "Optional, ONCE when you resume work: GET steps.check_payment. \"credited\" → open; \"pending\" → still AWAITING_PAYMENT: stop and check again only when you next resume. Never poll.",
      "next": "open",
      "note": "open again with a NEW Idempotency-Key"
    },
    {
      "step": "media",
      "do": "GET every gallery[].media_url with the same Bearer, right away (free while the concept is in your WINDOW).",
      "next": "done"
    }
  ],
  "steps": {
    "get_wallet": {
      "method": "POST",
      "href": "https://balance.nexa-ecosystem.com/v1/agent-wallets",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": {
        "label": "<your agent name: 1–40 characters A-Z a-z 0-9 space . _ ->"
      },
      "success": 201,
      "returns": {
        "balance_code": "NEXA-XX-… — store it privately, never send it to a site; it restores the wallet and issues new tokens",
        "token": "nbk_… — send it as Authorization: Bearer on every NEXA site",
        "token_id": "id of this token (for revoke by id)",
        "expires_at": "ISO time; default 30 days, max 90 (optional body field ttl_seconds, 60…7776000)"
      },
      "errors": {
        "400": "invalid label or ttl_seconds",
        "429": "too many new wallets right now — wait Retry-After seconds"
      },
      "cost": 0
    },
    "authenticate": {
      "header": "Authorization",
      "value": "Bearer <NEXA BALANCE API token>",
      "note": "No cookies are needed or used by machine clients."
    },
    "recover_token": {
      "method": "POST",
      "href": "https://balance.nexa-ecosystem.com/v1/api-tokens",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": {
        "code": "<balance_code>",
        "label": "<your agent name>"
      },
      "success": 201,
      "returns": {
        "token": "nbk_… — send it as Authorization: Bearer on every NEXA site",
        "token_id": "id of this token (for revoke by id)",
        "expires_at": "ISO time; default 30 days, max 90 (optional body field ttl_seconds, 60…7776000)"
      },
      "when": "your token expired, was revoked or lost — the wallet and its BALANCE stay",
      "errors": {
        "400": "invalid_code",
        "429": "wait Retry-After seconds"
      }
    },
    "rotate_token": {
      "method": "POST",
      "href": "https://balance.nexa-ecosystem.com/v1/api-tokens/rotate",
      "headers": {
        "Authorization": "Bearer <NEXA BALANCE API token>"
      },
      "success": 201,
      "returns": {
        "token": "nbk_… — send it as Authorization: Bearer on every NEXA site",
        "token_id": "id of this token (for revoke by id)",
        "expires_at": "ISO time; default 30 days, max 90 (optional body field ttl_seconds, 60…7776000)"
      },
      "when": "before expires_at; the presented token stops working at once"
    },
    "revoke_token": {
      "method": "POST",
      "href": "https://balance.nexa-ecosystem.com/v1/api-tokens/revoke",
      "headers": {
        "Authorization": "Bearer <NEXA BALANCE API token>"
      },
      "success": 200,
      "when": "the token leaked or is no longer needed"
    },
    "balance": {
      "method": "GET",
      "href": "https://www.nexa-ecosystem.com/api/balance/state",
      "headers": {
        "Authorization": "Bearer <NEXA BALANCE API token>"
      },
      "cost": 0,
      "returns": {
        "balance": "BALANCE of the wallet (shared by all its tokens and all NEXA sites)",
        "window": "content_ids open for THIS token (oldest first)",
        "products": "{ <content_id>: { status, free_access? } }",
        "free_positions": "the two FREE concepts of this site right now"
      }
    },
    "choose_package": {
      "from": "packages[] (id, price in EUR cents, BALANCE, valid_days)",
      "rule": "Any package whose balance covers what you need works; a bigger package lasts longer. Each purchase is valid valid_days days from its payment.",
      "suggested": "balance-720"
    },
    "buy": {
      "method": "POST",
      "href": "https://www.nexa-ecosystem.com/api/balance/checkout",
      "headers": {
        "Authorization": "Bearer <NEXA BALANCE API token>",
        "Content-Type": "application/json"
      },
      "body": {
        "package": "<package id from packages[]>"
      },
      "optional": {
        "rail": "\"checkout\" (default) or \"mpp\" — see payment_options"
      },
      "success": 200,
      "returns": {
        "status": "\"AWAITING_PAYMENT\" — your request is finished; do not wait",
        "checkout_url": "Stripe Checkout page (EUR, one-time payment, valid 24 hours)",
        "price": "{ currency, amount (cents) }",
        "balance": "BALANCE this purchase adds",
        "purchase": "reference for check_payment"
      },
      "returns_mpp": {
        "rail": "\"mpp\"",
        "pay_url": "the NEXA BALANCE MPP endpoint for exactly this purchase (payable for 30 minutes)",
        "price": "{ currency, amount (cents) }",
        "balance": "BALANCE this purchase adds",
        "purchase": "reference for check_payment",
        "expires_at": "ISO time"
      },
      "errors": {
        "400": "invalid_package, invalid rail, or rail \"mpp\" without an API token",
        "401": "token missing or rejected → recover_token",
        "429": "too many purchases started — wait Retry-After seconds",
        "503": "mpp_not_available"
      },
      "who_pays": "With rail \"mpp\" the agent pays itself (Shared Payment Token). With Stripe Checkout a person (the operator) or a system authorised to pay opens checkout_url and pays — an agent without a payment instrument cannot pay by itself. Nothing is charged automatically and nothing is charged by opening a URL.",
      "note": "An unpaid checkout_url expires after 24 hours, an unpaid pay_url after 30 minutes; call buy again for a new one. 24 hours is the validity of the link, not of your request: your request ends with the answer."
    },
    "pay": {
      "action": "open checkout_url and pay (card or another method Stripe offers)",
      "not_an_api_call": true,
      "credits": "only the verified Stripe webhook credits BALANCE; the return to the site credits nothing"
    },
    "pay_mpp": {
      "method": "POST",
      "href": "<pay_url from steps.buy with rail \"mpp\">",
      "protocol": "mpp (method \"stripe\", intent \"charge\")",
      "challenge": "402 with WWW-Authenticate: Payment method=\"stripe\", intent=\"charge\", request = amount (cents) / currency / networkId",
      "credential": "repeat the POST with Authorization: Payment <credential carrying a Shared Payment Token>",
      "returns": {
        "200": "paid — Payment-Receipt header (reference = Stripe PaymentIntent); body { status: \"paid\", purchase, balance_to_add }"
      },
      "errors": {
        "402": "declined or invalid credential — pay again with another payment method",
        "404": "expired (30 minutes) — call steps.buy again",
        "409": "already-paid / payment-in-progress (Retry-After)",
        "503": "payment-outcome-unknown — retry the same request (never charged twice) or check_payment"
      },
      "tools": [
        "link-cli mpp pay <pay_url> --method POST --context \"<what and why, ≥ 100 characters>\"",
        "mppx <pay_url> -X POST"
      ],
      "credits": "only the verified Stripe webhook payment_intent.succeeded credits BALANCE (seconds later) — request the concept again; if it still answers 402, check check_payment ONCE later. Never poll."
    },
    "check_payment": {
      "method": "GET",
      "href": "https://www.nexa-ecosystem.com/api/balance/purchase?id=<purchase>",
      "headers": {
        "Authorization": "Bearer <NEXA BALANCE API token>"
      },
      "success": 200,
      "returns": {
        "status": "\"pending\" (= AWAITING_PAYMENT) | \"credited\"",
        "package": "package id",
        "price": "{ currency, amount }",
        "balance_added": "BALANCE added by this purchase"
      },
      "when": "ONCE, when you resume work after a payment — never in a loop or periodically. \"pending\": stop and check again only when you next resume.",
      "errors": {
        "404": "unknown reference (or a purchase of another wallet)",
        "429": "checked too often — polling is not allowed; wait Retry-After seconds"
      }
    },
    "media": {
      "method": "GET",
      "href": "https://www.nexa-ecosystem.com/api/catalog/media/{asset_id}",
      "use": "gallery[].media_url of the concept response (every image, the cover too)",
      "headers": {
        "Authorization": "Bearer <NEXA BALANCE API token>"
      },
      "cost": 0,
      "condition": "only while the concept is in your WINDOW — fetch the images right after the concept",
      "errors": {
        "401": "token missing or rejected",
        "403": "locked: the concept left your WINDOW — open it again (one PRODUCT VIEW)"
      }
    }
  },
  "errors": {
    "401": {
      "error": "credential_required",
      "meaning": "no token, or the token is invalid / expired / revoked",
      "do": "get_wallet or recover_token, then repeat"
    },
    "402": {
      "error": "balance_required",
      "meaning": "too little BALANCE for this PRODUCT VIEW",
      "do": "choose_package → buy → pay → check_payment → repeat"
    },
    "403": {
      "error": "locked",
      "meaning": "image of a concept that is not in your WINDOW",
      "do": "open the concept again, then fetch the image"
    },
    "404": {
      "error": "concept_not_found | media_not_found",
      "meaning": "unknown id",
      "do": "use ids from the free catalog"
    },
    "405": {
      "error": "method_not_allowed",
      "do": "see the Allow header"
    },
    "409": {
      "error": "idempotency_key_reused | already-paid | payment-in-progress (pay_url)",
      "meaning": "the Idempotency-Key was already used for a different request; or the purchase is paid, or a payment for it is being processed",
      "do": "a new Idempotency-Key for every new request (repeat a key only for the identical request); check_payment, never pay a purchase twice"
    },
    "429": {
      "error": "rate_limited",
      "meaning": "only on wallet / token creation",
      "do": "wait Retry-After seconds"
    },
    "503": {
      "error": "balance_unavailable | payment-outcome-unknown (pay_url)",
      "meaning": "NEXA BALANCE or Stripe did not answer; nothing protected is served, nothing is charged twice",
      "do": "wait (Retry-After) and repeat the same request"
    }
  },
  "response_headers": {
    "X-NEXA-Charged": "BALANCE charged by this request (0 or the PRODUCT VIEW price)",
    "X-NEXA-Balance": "BALANCE left after this request",
    "X-NEXA-Window": "content_ids in your WINDOW, oldest first",
    "ETag": "content_hash of the concept / catalog_hash of the list",
    "Link": "this document (rel=\"describedby\") and the OpenAPI description (rel=\"service-desc\")"
  },
  "paths": {
    "ai_agent_with_payment": [
      "POST steps.get_wallet → keep balance_code private, use token as Bearer",
      "GET the free catalog; choose concepts by version / content_hash",
      "GET a concept with the token and an Idempotency-Key → 200 (content) or 402 (packages + steps)",
      "POST steps.buy with rail \"mpp\" → POST the returned pay_url and answer its 402 with a Shared Payment Token (steps.pay_mpp) → 200 + Payment-Receipt",
      "request the concept again (new Idempotency-Key); only if it still answers 402, GET steps.check_payment ONCE later — never poll",
      "GET every gallery[].media_url right away"
    ],
    "ai_agent_without_payment": [
      "POST steps.buy (rail \"checkout\") → checkout_url + purchase + status AWAITING_PAYMENT; your request is finished",
      "Hand checkout_url to a person or a system authorised to pay (valid 24 hours). Do not wait, do not poll, keep no connection open.",
      "The verified Stripe webhook credits the BALANCE to your wallet on its own after the payment.",
      "When you resume work later: request the concept again with the same token (new Idempotency-Key), or GET steps.check_payment ONCE."
    ],
    "crawler_with_operator": [
      "The crawler gets 401/402 with this document linked (no checkout is created automatically).",
      "The operator: POST steps.get_wallet, POST steps.buy, opens checkout_url in a browser and pays (EUR, one-time).",
      "The operator configures the crawler with Authorization: Bearer <token> and keeps the balance_code private.",
      "The crawler uses the free catalog to fetch only new or changed concepts, then each concept followed by its images.",
      "When the token nears expires_at: POST steps.rotate_token (or steps.recover_token with the balance_code)."
    ],
    "people": "In the browser: the site shows the packages, the Balance Code and Stripe Checkout itself."
  },
  "links": {
    "openapi": "https://www.nexa-ecosystem.com/.well-known/nexa-balance.openapi.json",
    "llms": "https://www.nexa-ecosystem.com/llms.txt",
    "nexa_balance": {
      "openapi": "https://balance.nexa-ecosystem.com/openapi.json",
      "llms": "https://balance.nexa-ecosystem.com/llms.txt"
    }
  }
}