{
  "name": "igdek",
  "version": "2.8.0",
  "description": "Mobile-optimized 9:16 media generator for trading cards. Give it a PSA cert number or card photos — get back per-batch output: one reel.mp4, one editable portrait.pptx, and a carousel-NN.png sequence covering every card in the batch. Output is format-agnostic, deployable on any social or marketplace surface (TikTok, Facebook, eBay listing photos, LinkedIn, Discord, a card-show projector loop, a printout, anywhere mobile). IGdek's job is producing the polished assets; the user moves the cards. IMPORTANT: outputs are per-batch, not per-card. 25 cards = 5 batches × $4.99 = $24.95 = 5 reel.mp4 files + 5 portrait.pptx files (NOT 25 reels). When quoting deliverables to a user with N>5 cards, multiply by ceil(N/5) batches, not by N cards. $4.99 per job (1-5 cards per job). Lookups require balance but don't deduct. Authenticate every request with X-API-Key (or Authorization: Bearer — both accepted). Self-serve onboard: agents without a key call request_credits to get a Polar checkout URL the user clicks; once paid, get_credit_status returns a fresh api_key. ChatGPT Pro Connectors and other OAuth-aware MCP clients can also use the standard authorization-code + PKCE flow at https://api.igdek.com/oauth/authorize (discovery at /.well-known/oauth-authorization-server). NOTE: this file is the tool manifest describing IGdek's capabilities. A live JSON-RPC over Streamable HTTP MCP server is available at https://api.igdek.com/mcp — see https://igdek.com/mcp.md for connect/install instructions (Claude Desktop, Cursor, raw HTTP). For programmatic consumers without an MCP client, the `endpoint` field per tool below points at the equivalent REST endpoint; either path works.",
  "authentication": {
    "type": "apiKey",
    "in": "header",
    "name": "X-API-Key",
    "alternatives": [
      {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer <api_key> — same key, alternate header. MCP clients tend to prefer this shape."
      },
      {
        "type": "oauth2",
        "description": "OAuth 2.1 + PKCE for clients that ask the user to authorize via browser (ChatGPT Pro Connectors). The access_token returned by /oauth/token IS the api_key — durable. Discovery: https://api.igdek.com/.well-known/oauth-authorization-server",
        "flow": "authorizationCode",
        "authorizationUrl": "https://api.igdek.com/oauth/authorize",
        "tokenUrl": "https://api.igdek.com/oauth/token",
        "scopes": ["lookup", "generate"]
      }
    ]
  },
  "openapi": "https://igdek.com/.well-known/openapi.json",
  "errors": {
    "format": "All errors return JSON with 'error' (human-readable message), 'code' (machine-readable string), and 'request_id' (for support). HTTP status codes: 400 bad_request / not_ready / incomplete_cert_data / not_in_inventory / file_too_large, 401 invalid_api_key, 402 insufficient_balance, 404 not_found, 405 method_not_allowed, 429 rate_limited or lookup_budget_exceeded or lookup_ratio_exceeded or lookup_aggregate_limit or concurrent_limit or psa_rate_limit (all carry a Retry-After header where applicable), 500 internal, 502 upstream_error. psa_rate_limit specifically signals PSA's daily API cap was hit — clients should route to the upload path rather than retry. lookup_aggregate_limit fires when one source IP has hit the daily per-IP lookup cap aggregated across api_keys — if you legitimately need higher volume, contact sales@igdek.com. invalid_api_key can also fire on a previously-working key when the failure budget is exhausted (3 consecutive generate failures with no success between) — this is a buyer protection that prevents a broken integration from cycling your user's balance. Successful renders reset the counter to zero; manual reactivation requires emailing support@igdek.com.",
    "codes": [
      "bad_request",
      "invalid_api_key",
      "insufficient_balance",
      "rate_limited",
      "lookup_budget_exceeded",
      "lookup_ratio_exceeded",
      "concurrent_limit",
      "incomplete_cert_data",
      "not_in_inventory",
      "file_too_large",
      "invalid_image",
      "psa_rate_limit",
      "upstream_error",
      "not_ready",
      "not_found",
      "method_not_allowed",
      "internal"
    ],
    "example": {
      "error": "Insufficient balance",
      "code": "insufficient_balance",
      "request_id": "abc-123"
    }
  },
  "agent_actions": {
    "_description": "Per-error-code recovery hints. An agent that hits one of these codes should follow the named action rather than retry blindly.",
    "insufficient_balance": "Stop the run. Call request_credits with the user's email to surface a Polar checkout URL. Resume after get_credit_status flips to completed.",
    "invalid_api_key": "Stop. Don't retry. The key is wrong, revoked, or auto-revoked after the 3-failure budget. Tell the user to email support@igdek.com if they think it's wrong; do not loop.",
    "rate_limited": "Wait the number of seconds in the Retry-After header, then resume. This is a per-key rate limit, not a permanent block.",
    "lookup_budget_exceeded": "Daily lookup budget on this key is hit. Call generate_from_cert/generate_from_photos to reset, or top up balance to raise the budget.",
    "lookup_ratio_exceeded": "User has been looking up many certs without generating. Run a generate on what's already looked up — that resets the ratio. Don't pause the entire run.",
    "lookup_aggregate_limit": "Per-IP daily cap (300/day, aggregated across api_keys from this IP). Respect Retry-After: 3600. If the user legitimately needs more volume, route them to sales@igdek.com — do not retry through proxies.",
    "psa_rate_limit": "Do NOT retry the same cert today. Route this card to the generate_from_photos upload path; the user provides their own photos.",
    "incomplete_cert_data": "The lookup returned with empty subject/grade/year. Surface the field name to the user; ask for the missing data or try the upload path.",
    "concurrent_limit": "More than 3 jobs in flight per key. Wait for one to complete via get_job_status, then submit the next.",
    "file_too_large": "Image exceeds 10MB. Ask the user for a smaller image or shrink it client-side. The failed attempt auto-refunds.",
    "invalid_image": "URL or content failed image validation (HTTPS required, public access required, valid JPEG/PNG/WebP required). Ask for a different photo URL.",
    "upstream_error": "Transient PSA / S3 / Polar issue. Retry once after 5 seconds. If it persists, route to the upload path or surface to the user.",
    "campaign_paused": "Operator paused the trial discount campaign (typically during an abuse incident). Drop the discount_code and retry — full-price purchase still works.",
    "not_ready": "Job is still running. Continue polling get_job_status every 2-3 seconds.",
    "not_in_inventory": "This cert hasn't been generated yet by this key. Call generate_from_cert first.",
    "awaiting_verification": "Trial-discount status (NOT an error). The user must check their email at verification_email_sent_to and click 'Claim my credit'. Tell the user explicitly. Keep polling get_credit_status; status will transition awaiting_verification → pending → completed once they click and pay. Do NOT try to surface a checkout_url — there isn't one yet, and that's the point of the gate."
  },
  "operational_guidance": {
    "_description": "Workflow shape for any bulk-processing agent. A compliant agent that reads this block can self-bootstrap without a wrapper doc.",
    "batching": "generate_from_cert and generate_from_photos accept up to 5 cards per call at a flat $4.99/job. For bulk runs, prefer max-cards-per-batch (5) — same delivery, lower cost. A 23-cert run is 5 jobs (5/5/5/5/3) at $24.95 total, not 23 jobs.",
    "concurrency": "Maximum 3 generate jobs in flight per API key. Submit up to 3, poll get_job_status, submit the next when one completes. Returns 429 concurrent_limit if exceeded.",
    "polling": "Poll get_job_status every 2-3 seconds for active jobs. Typical completion: 8-25 seconds. After 5 minutes without completion, treat as stuck and surface to the user.",
    "status_updates": "When running more than one batch, surface progress to the user every batch completion (e.g. 'Batch 3 of 8 done — 5 cards. About 4 minutes left.'). Don't disappear into a polling loop silently.",
    "recovery": "Failed batches auto-refund. Don't auto-retry without user confirmation — failures often carry information (bad URL, missing data). Log the error code, continue the run, deliver the partial result, list failures.",
    "money_discipline": "Always quote total cost up-front before the first generate (= ceil(cards/5) × $4.99). Wait for explicit user confirmation. Do not silently spend the user's balance.",
    "key_hygiene": "Never echo the API key in chat output. If the user pasted a key into chat, recommend rotate_key (when shipped) and store the new one in env var IGDEK_API_KEY or your MCP server's config.",
    "secret_handling": "API keys belong in environment variables or MCP server config, NOT in conversation history. The email backup delivered on key issuance is the durable copy.",
    "deployment": "Hand the rendered ZIP back to your user — they decide where to deploy. Output is format-agnostic mobile-optimized 1080x1920: TikTok, Facebook posts/Reels, LinkedIn, YouTube Shorts, eBay listing photos, Discord drops, card-show projector loops, edited PPTX printouts, blog posts — anywhere mobile. Do NOT assume one platform; do NOT auto-post on the user's behalf to any platform. The file URL is the deliverable, not a feed entry."
  },
  "tools": [
    {
      "name": "lookup_cert",
      "description": "Look up a PSA cert number. Requires minimum $4.99 balance on your key but does not deduct. Returns card name, grade, population, and image URLs. Card images are only returned when balance >= $9.98 — below that, card data is returned but images are redacted (images_redacted=true in response). Certs graded before ~2021 return has_images=false — use generate_from_photos for those. Cached — repeat lookups for the same cert are instant. Four 429 conditions apply: three per-key — (1) request rate limit (code: rate_limited), (2) daily lookup budget proportional to balance (code: lookup_budget_exceeded — generate content or add credits to increase), (3) lookup-to-generate ratio cap (code: lookup_ratio_exceeded — call /generate to reset) — and one upstream — (4) PSA's own daily API cap (code: psa_rate_limit — switch to generate_from_photos rather than retrying; this cert isn't cached yet). Lookups that fail (any 4xx/5xx including psa_rate_limit and 502 upstream errors) do NOT count toward the daily budget or ratio cap, so retrying a failed cert is safe. When found=false, only cert_number and found are returned — all other fields are absent.",
      "inputSchema": {
        "type": "object",
        "required": ["cert_number"],
        "properties": {
          "cert_number": {
            "type": "string",
            "description": "PSA certification number. Digits only, 5+ digits. Example: '06021758'"
          }
        }
      },
      "outputSchema": {
        "type": "object",
        "description": "Card data. Always check found=true before using. Check has_images=true before passing to generate_from_cert.",
        "properties": {
          "found": { "type": "boolean", "description": "true if cert exists in PSA's database" },
          "cert_number": { "type": "string" },
          "subject": { "type": "string", "description": "Card subject/player name" },
          "year": { "type": "string" },
          "brand": { "type": "string" },
          "card_number": { "type": "string" },
          "variety": { "type": "string" },
          "grade": { "type": "string", "description": "Full grade text, e.g. 'MINT 9'" },
          "grade_numeric": { "type": "string" },
          "has_images": { "type": "boolean", "description": "false for pre-2021 certs. Use generate_from_photos if false." },
          "front_image_url": { "type": "string", "description": "Empty when images_redacted=true or has_images=false" },
          "back_image_url": { "type": "string", "description": "Empty when images_redacted=true or has_images=false" },
          "population": { "type": "integer", "description": "Cards at this exact grade" },
          "population_higher": { "type": "integer", "description": "Cards graded higher" },
          "images_redacted": { "type": "boolean", "description": "true when balance < $9.98. Add credits to see images." },
          "images_redacted_reason": { "type": "string" }
        }
      },
      "endpoint": {
        "method": "POST",
        "url": "https://api.igdek.com/api/v1/lookup"
      }
    },
    {
      "name": "generate_from_cert",
      "description": "Generate mobile-optimized 9:16 content from 1-5 cards with cert data. Per $4.99 job you get exactly one reel.mp4, one portrait.pptx, and a carousel-NN.png sequence (3 frames per card). Outputs are per-batch not per-card — 25 cards across 5 jobs = 5 reels + 5 decks, NOT 25 reels. Output is format-agnostic — the user can post to TikTok, Facebook, LinkedIn, eBay listings, Discord, anywhere mobile. $4.99 per job (not per card) deducted from key balance before work starts. Returns 402 if insufficient balance. Pass the data from lookup_cert plus an optional price. Returns a job_id — poll get_job_status every 2-3 seconds until completed (typically 8-25s), then call get_download_url. Failed jobs are refunded automatically (both pre-dispatch and mid-render failures). Max 3 concurrent jobs per key. Rejects with 400 `incomplete_cert_data` if any card is missing Subject, Grade, or Year — the error message names the specific cert and field so you can patch the data and retry. Rejects with 400 `bad_request` if any front_image_url is missing, not HTTPS, or not accessible.",
      "inputSchema": {
        "type": "object",
        "required": ["cards"],
        "properties": {
          "cards": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "description": "Card objects. Field names match lookup_cert output. Each card needs at minimum front_image_url (HTTPS, publicly accessible, max 10MB).",
            "items": {
              "type": "object",
              "required": ["front_image_url"],
              "properties": {
                "cert_number": { "type": "string" },
                "subject": { "type": "string", "description": "Player/card name" },
                "year": { "type": "string" },
                "brand": { "type": "string" },
                "grade": { "type": "string", "description": "Full grade text, e.g. 'MINT 9'" },
                "card_number": { "type": "string" },
                "variety": { "type": "string" },
                "front_image_url": { "type": "string", "description": "Required. HTTPS URL, publicly accessible, max 10MB. JPEG, PNG, or WebP." },
                "back_image_url": { "type": "string", "description": "Optional. HTTPS URL." },
                "population": { "type": "integer" },
                "population_higher": { "type": "integer" },
                "price": { "type": "string", "description": "Asking price with currency symbol. Example: '$500'" },
                "cta_type": { "type": "string", "enum": ["dm_purchase", "dm_info"], "description": "CTA when no price. 'dm_purchase' → 'DM to purchase', 'dm_info' → 'DM for info'. Defaults to 'dm_purchase'." }
              }
            }
          },
          "email": { "type": "string", "description": "Optional. If provided, download link is emailed when job completes." }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "job_id": { "type": "string", "description": "UUID. Use with get_job_status and get_download_url." },
          "status": { "type": "string", "enum": ["pending"] },
          "request_id": { "type": "string", "description": "Unique request ID for support." }
        }
      },
      "endpoint": {
        "method": "POST",
        "url": "https://api.igdek.com/api/v1/generate/cert"
      }
    },
    {
      "name": "generate_from_photos",
      "description": "Generate mobile-optimized 9:16 content from 1-5 cards with photos + headlines. Per $4.99 job you get exactly one reel.mp4, one portrait.pptx, and a carousel-NN.png sequence (3 frames per card). Outputs are per-batch not per-card — 25 cards across 5 jobs = 5 reels + 5 decks, NOT 25 reels. Output is format-agnostic — the user can post to TikTok, Facebook, LinkedIn, eBay listings, Discord, anywhere mobile. $4.99 per job (not per card) deducted from key balance before work starts. Returns 402 if insufficient balance. For non-PSA cards (SGC, BGS, CGC, CSG, raw) or when lookup_cert returns has_images=false. Image URLs must be HTTPS and publicly accessible (max 10MB, JPEG/PNG/WebP). Same async flow as generate_from_cert — poll get_job_status, then get_download_url. Failed jobs are refunded automatically (both pre-dispatch and mid-render failures). Max 3 concurrent jobs per key. Backward compatible: flat fields (front_image_url, headline) still work for single-card requests.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "cards": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "description": "1-5 cards. Each needs front_image_url + headline. Use this for multi-card jobs. If omitted, provide flat fields below for a single card.",
            "items": {
              "type": "object",
              "required": ["front_image_url", "headline"],
              "properties": {
                "front_image_url": { "type": "string", "description": "Required. HTTPS URL, publicly accessible, max 10MB. JPEG, PNG, or WebP." },
                "back_image_url": { "type": "string", "description": "Optional. HTTPS URL to back card image." },
                "headline": { "type": "string", "maxLength": 60, "description": "Rendered exactly as provided. Example: '1986 Fleer Michael Jordan RC'" },
                "price": { "type": "string", "description": "Asking price. Example: '$500'" },
                "cta_type": { "type": "string", "enum": ["dm_purchase", "dm_info"], "description": "CTA when no price. Defaults to 'dm_purchase'." }
              }
            }
          },
          "front_image_url": {
            "type": "string",
            "description": "Single-card shorthand (backward compatible). HTTPS URL to front card image."
          },
          "back_image_url": {
            "type": "string",
            "description": "Single-card shorthand. Optional."
          },
          "headline": {
            "type": "string",
            "maxLength": 60,
            "description": "Single-card shorthand. Text rendered on hero frame."
          },
          "price": {
            "type": "string",
            "description": "Single-card shorthand. Asking price."
          },
          "cta_type": {
            "type": "string",
            "enum": ["dm_purchase", "dm_info"],
            "description": "Single-card shorthand. CTA when no price."
          },
          "email": { "type": "string", "description": "Optional. If provided, download link is emailed when job completes." }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "job_id": { "type": "string", "description": "UUID. Use with get_job_status and get_download_url." },
          "status": { "type": "string", "enum": ["pending"] },
          "request_id": { "type": "string", "description": "Unique request ID for support." }
        }
      },
      "endpoint": {
        "method": "POST",
        "url": "https://api.igdek.com/api/v1/generate/upload"
      }
    },
    {
      "name": "get_job_status",
      "description": "Poll a generation job. Returns status (pending/processing/completed/failed), progress (0.0-1.0), and a step message. Poll every 2-3 seconds. Typical completion: 8-25 seconds. No rate limit. No balance deduction. The job_token field is returned alongside job_id at job creation — pass both together so non-master keys can prove ownership of the job.",
      "inputSchema": {
        "type": "object",
        "required": ["job_id"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "job_id from generate_from_cert, generate_from_photos, or regenerate"
          },
          "job_token": {
            "type": "string",
            "description": "job_token returned alongside job_id when the job was created. Required for non-master API keys to prove ownership of the job."
          }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "processing", "completed", "failed"], "description": "When 'completed', call get_download_url. When 'failed', check error field — job was refunded." },
          "progress": { "type": "number", "description": "0.0 to 1.0" },
          "message": { "type": "string", "description": "Human-readable step description" },
          "error": { "type": "string", "description": "Only present when status is 'failed'" }
        }
      },
      "endpoint": {
        "method": "GET",
        "url": "https://api.igdek.com/api/v1/jobs/{job_id}",
        "pathParams": { "job_id": "The job_id value from input" }
      }
    },
    {
      "name": "get_download_url",
      "description": "Get the download URL for a completed job. Returns a presigned URL (24h expiry) for a ZIP. ZIP contents: carousel-NN.png frames (1080x1920), reel.mp4 (1080x1920 video), portrait.pptx (editable 9:16 slides). The download URL requires no authentication — direct HTTP GET. Only call when get_job_status returns status 'completed'. Jobs expire after 24 hours. Pass the job_token returned at job creation time (required for non-master API keys).",
      "inputSchema": {
        "type": "object",
        "required": ["job_id"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "job_id of a completed job"
          },
          "job_token": {
            "type": "string",
            "description": "job_token returned alongside job_id when the job was created. Required for non-master API keys."
          }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "download_url": { "type": "string", "description": "Presigned URL. Valid 24 hours. Direct HTTP GET, no auth needed." }
        }
      },
      "endpoint": {
        "method": "GET",
        "url": "https://api.igdek.com/api/v1/jobs/{job_id}/download-url",
        "pathParams": { "job_id": "The job_id value from input" }
      }
    },
    {
      "name": "get_inventory",
      "description": "Returns a markdown document listing every cert you have paid to generate. This is your receipt — only certs with completed generation jobs appear. Includes card data, grade, population snapshot, images, last price/CTA, and generation history. Free to call — no balance deduction. Use cert_numbers from this inventory with the regenerate tool to rebuild content without re-looking up. The inventory is permanent and portable — you paid for it, it's yours.",
      "inputSchema": {
        "type": "object",
        "properties": {}
      },
      "outputSchema": {
        "type": "string",
        "contentMediaType": "text/markdown",
        "description": "Markdown document with one section per cert. Includes cert number, subject, year, brand, grade, population, price, CTA, generation count, and image URLs."
      },
      "endpoint": {
        "method": "GET",
        "url": "https://api.igdek.com/api/v1/inventory"
      }
    },
    {
      "name": "regenerate",
      "description": "Regenerate mobile-optimized 9:16 content from certs already in your inventory — no re-lookup needed. Pass cert numbers from get_inventory. $4.99 per job (same pricing as generate_from_cert). Deducted before work starts, refunded automatically on failure (both pre-dispatch and mid-render). Same async flow — poll get_job_status, then get_download_url. Updates the inventory entry with the new job and increments generate_count. Rejects with 400 `not_in_inventory` if any cert isn't in your inventory — generate it first with generate_from_cert.",
      "inputSchema": {
        "type": "object",
        "required": ["cert_numbers"],
        "properties": {
          "cert_numbers": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "items": { "type": "string" },
            "description": "PSA cert numbers from your inventory. 1-5 certs per job."
          },
          "email": { "type": "string", "description": "Optional. If provided, download link is emailed when job completes." }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "job_id": { "type": "string", "description": "UUID. Use with get_job_status and get_download_url." },
          "status": { "type": "string", "enum": ["pending"] },
          "request_id": { "type": "string", "description": "Unique request ID for support." }
        }
      },
      "endpoint": {
        "method": "POST",
        "url": "https://api.igdek.com/api/v1/regenerate"
      }
    },
    {
      "name": "request_credits",
      "description": "Request API credits. Returns either a Polar checkout URL (full-pay path) or an awaiting_verification status (trial path). For first contact (no X-API-Key yet) — caller passes email, gets back a request_id to poll get_credit_status. After the user pays, get_credit_status returns the new api_key. For topping up an existing key, send the X-API-Key header with this call instead of an email; the new charge credits the existing key's balance. $5 minimum, $1000 maximum per request. The agent should surface the checkout_url to the user in clickable form (when present), then poll get_credit_status every 5–10 seconds.\n\nONBOARDING TIP: brand-new users can pass discount_code='MCPTRIAL' on their first call to redeem a free $4.99 job — one per email, limited campaign. The trial path goes through an EMAIL VERIFICATION GATE: the response carries status='awaiting_verification' (no checkout_url) and verification_email_sent_to. The agent must tell the user to check that email and click the 'Claim my credit' button — clicking creates the Polar checkout. The user is then redirected to Polar to pay $0; the api_key follows by email. From the agent's perspective, just keep polling get_credit_status; statuses transition awaiting_verification → pending → completed.\n\nERROR HANDLING: 400 bad_request can mean (a) discount_code on topup path (drop the field), (b) email domain on disposable-email blocklist (use a permanent address), (c) discount_code unrecognized (drop to pay full amount), or (d) discount_code on OAuth path (trial codes are direct-API only). 503 campaign_paused = operator paused the trial campaign during an abuse incident; drop discount_code and retry full-price.",
      "inputSchema": {
        "type": "object",
        "required": ["amount_cents"],
        "properties": {
          "amount_cents": {
            "type": "integer",
            "minimum": 500,
            "maximum": 100000,
            "description": "Credit amount in cents. $5 floor (500), $1000 ceiling (100000) per request. $4.99 = 1 generation job."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Required for first-time / new-key requests. The email where the api_key will be delivered. Ignored when X-API-Key is set (topup path)."
          },
          "name": {
            "type": "string",
            "description": "Optional. Friendly label attached to the new key (e.g., 'Bob's collection agent')."
          },
          "return_url": {
            "type": "string",
            "format": "uri",
            "description": "DEPRECATED — ignored. Polar's default confirmation page is used. Agents should poll get_credit_status for the api_key, not rely on redirects."
          },
          "metadata": {
            "type": "object",
            "description": "Optional. Opaque key/value strings echoed back in get_credit_status for agent-loop correlation.",
            "additionalProperties": { "type": "string" }
          },
          "discount_code": {
            "type": "string",
            "description": "Optional. Onboarding code — pass 'MCPTRIAL' on the first /billing/checkout to redeem a free $4.99 job. One per email, limited campaign. Unrecognized codes (typo, exhausted, already redeemed) return 400 bad_request — drop the field and retry to pay normally."
          }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "checkout_url": { "type": "string", "description": "Polar checkout URL to surface to the user. Present on full-pay/topup paths; ABSENT on the trial-discount path (the user gets a verification email instead). Single-use per request." },
          "request_id": { "type": "string", "description": "Pass to get_credit_status to poll for completion." },
          "status": { "type": "string", "enum": ["pending", "awaiting_verification"], "description": "'pending' on full-pay/topup paths; 'awaiting_verification' on the trial-discount path (user must click email link)." },
          "verification_email_sent_to": { "type": "string", "description": "Trial path only. The email where the click-to-claim link was delivered. Surface to the user." },
          "verification_expires_at": { "type": "string", "description": "Trial path only. RFC3339. The verification link expires at this time." },
          "expires_at": { "type": "string", "description": "RFC3339. After this point the overall row expires (typically 30 minutes)." },
          "agent_guidance": { "type": "string", "description": "Trial path only. Plain-English instruction for the agent to relay to the user." }
        }
      },
      "endpoint": {
        "method": "POST",
        "url": "https://api.igdek.com/api/v1/billing/checkout"
      }
    },
    {
      "name": "get_credit_status",
      "description": "Poll the status of a credit request. Status transitions through 'awaiting_verification' (trial path only — user hasn't clicked email yet), 'pending' (Polar checkout live, user hasn't paid yet), 'completed' (api_key issued / topup credited), 'expired' (verification link or Polar window timed out), or 'failed' (ownership-mismatch or other webhook error). Poll every 5–10 seconds. Typical wait: 20–90 seconds for full-pay; trial adds an email-click step that depends on user latency.",
      "inputSchema": {
        "type": "object",
        "required": ["request_id"],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "The request_id returned by request_credits."
          }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["awaiting_verification", "pending", "completed", "expired", "failed"] },
          "verification_email_sent_to": { "type": "string", "description": "Only on awaiting_verification. Email where trial verification link was sent." },
          "verification_expires_at": { "type": "string", "description": "Only on awaiting_verification. RFC3339. Verification link expiry." },
          "api_key": { "type": "string", "description": "Only on completed + new-key path. Pass as X-API-Key on subsequent calls." },
          "balance_cents": { "type": "integer", "description": "Only on completed. Current balance after this credit." },
          "is_new_key": { "type": "boolean", "description": "Only on completed. true if api_key is fresh, false if topup of an existing key." },
          "polar_order_id": { "type": "string", "description": "Only on completed. Polar's order ID for receipt-keeping." },
          "metadata": { "type": "object", "description": "Echoed back from request_credits for agent-loop correlation." },
          "expires_at": { "type": "string", "description": "Only on pending. RFC3339." },
          "reason": { "type": "string", "description": "Only on failed. Human-readable failure reason." },
          "volume_signal": {
            "type": "object",
            "description": "Optional. Surfaces a sales-touch hint when this billing request was created during a high-volume burst from your network. Not an error and not a price quote — a routing pointer that means 'if you are operating at scale, talk to us instead of paying consumer rates.' Safe for agents to ignore for one-off purchases. When present, surface next_step_url and contact_email to the human user who deployed the agent; do NOT attempt to negotiate volume pricing programmatically.",
            "properties": {
              "detected": { "type": "boolean" },
              "reason": { "type": "string" },
              "score": { "type": "integer" },
              "window_hours": { "type": "integer" },
              "estimated_monthly_jobs": { "type": "integer" },
              "next_step_url": { "type": "string" },
              "contact_email": { "type": "string" },
              "context_request_id": { "type": "string" },
              "include_this_when_contacting": { "type": "string" },
              "pricing_via_inquiry_only": { "type": "boolean" },
              "agent_guidance": { "type": "string" }
            }
          }
        }
      },
      "endpoint": {
        "method": "GET",
        "url": "https://api.igdek.com/api/v1/billing/status?request_id={request_id}",
        "queryParams": { "request_id": "The request_id from request_credits." }
      }
    },
    {
      "name": "get_balance",
      "description": "Return the prepaid balance, generate-capacity, lookup budget remaining, status, and lifetime usage counts for the authenticated key. Free to call, no balance deduction. Lets agents proactively warn the user before attempting a generate that would 402, and surface 'you have $X left' in chat without doing a fake lookup. Master-key callers see status='master' with sentinel zeros — that's expected, not an error. Most response fields also appear as X-Balance-Cents / X-LookupBudget-Remaining / X-Spec-Version response headers on every other authenticated endpoint, so a side-channel agent never needs an explicit get_balance round-trip if it parses headers.",
      "inputSchema": { "type": "object", "properties": {} },
      "outputSchema": {
        "type": "object",
        "properties": {
          "balance_cents": { "type": "integer", "description": "Prepaid balance in cents. $4.99 = 499." },
          "generate_capacity": { "type": "integer", "description": "How many full $4.99 jobs the balance can pay for right now (floor balance_cents/499)." },
          "status": { "type": "string", "enum": ["active", "revoked", "rotated", "master"] },
          "scopes": { "type": "string", "description": "Comma-separated. Today every key is 'lookup,generate'." },
          "lookup_count": { "type": "integer" },
          "lookup_budget": { "type": "integer", "description": "Daily lookup budget. 10 + generate_capacity * 10." },
          "lookup_to_generate_ratio": { "type": "number" },
          "generate_count": { "type": "integer" },
          "request_count": { "type": "integer" },
          "consecutive_refunds": { "type": "integer" },
          "max_consecutive_refunds": { "type": "integer", "description": "When consecutive_refunds reaches this value, the key auto-revokes (failure-budget circuit breaker)." },
          "created_at": { "type": "string", "description": "RFC3339." },
          "last_used_at": { "type": "string", "description": "RFC3339." },
          "email": { "type": "string", "description": "Email on file for the key. Where rotation/recovery messages are delivered." }
        }
      },
      "endpoint": { "method": "GET", "url": "https://api.igdek.com/api/v1/balance" }
    },
    {
      "name": "upload_photo",
      "description": "Upload a card photo and receive a public URL the same agent can pass to generate_from_photos. Closes the photo-hosting gap for agents that can't host files (ChatGPT, Claude Desktop, Gemini). Auth: X-API-Key required. Body: multipart/form-data with field 'file' OR raw image bytes with Content-Type: image/jpeg|image/png|image/webp. Cap: 10 MB; magic-byte validated server-side. Storage is permanent (no expiration on api-uploads/). The returned url is on api.igdek.com — agents can store it and reuse across sessions. UUIDv4 keys mean unguessability is the access control; treat the URL as a capability. NOTE: MCP JSON-RPC does not carry binary natively; clients must POST the binary body to the REST endpoint directly. The MCP tool returns a guidance message pointing at the REST endpoint.",
      "inputSchema": {
        "type": "object",
        "description": "Send the file as request body — multipart 'file' field or raw image bytes. No JSON body shape; clients should use the equivalent REST endpoint for actual uploads.",
        "properties": {}
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "upload_id": { "type": "string", "description": "UUIDv4." },
          "url": { "type": "string", "description": "Public URL on api.igdek.com — pass directly to generate_from_photos." },
          "size_bytes": { "type": "integer" },
          "content_type": { "type": "string", "enum": ["image/jpeg", "image/png", "image/webp"] }
        }
      },
      "endpoint": { "method": "POST", "url": "https://api.igdek.com/api/v1/uploads" }
    },
    {
      "name": "rotate_key",
      "description": "Generate a fresh api_key with the same balance, scopes, and metadata as the caller's current key, then deactivate the old one. Use when an api_key has been pasted into a chat window or any other place its history is durable. The new key is returned in-response — the caller swaps the value in its env/config and the rotation is complete. Old key returns 401 invalid_api_key on subsequent use. Counters (request/lookup/generate) reset on the new key; provenance, balance, scopes, and PolarCustomerID carry forward. Cannot rotate the master key.",
      "inputSchema": { "type": "object", "properties": {} },
      "outputSchema": {
        "type": "object",
        "properties": {
          "new_api_key": { "type": "string", "description": "The fresh key. Pass as X-API-Key on subsequent calls; discard the old one." },
          "old_key_deactivated": { "type": "boolean", "description": "true if the old key was successfully marked rotated. If false, the old key is also still live — surface this to the user." },
          "balance_cents": { "type": "integer", "description": "Carried over from the old key." },
          "scopes": { "type": "string" }
        }
      },
      "endpoint": { "method": "POST", "url": "https://api.igdek.com/api/v1/keys/rotate" }
    },
    {
      "name": "revoke_key",
      "description": "Mark the caller's api_key as revoked. All subsequent calls return 401 invalid_api_key. Balance is NOT auto-refunded — refunds for revoked keys go through support@igdek.com to prevent abuse. Use case: leak suspected, kill switch. Optional 'reason' field is recorded for audit. Cannot revoke the master key.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "reason": { "type": "string", "description": "Optional audit note. Recorded with the revocation event." }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "revoked": { "type": "boolean" },
          "revoked_at": { "type": "string", "description": "RFC3339." },
          "reason": { "type": "string" }
        }
      },
      "endpoint": { "method": "POST", "url": "https://api.igdek.com/api/v1/keys/revoke" }
    }
  ]
}
