{
  "openapi": "3.1.0",
  "info": {
    "title": "Laso Finance x402 API",
    "version": "1.0.0",
    "x-docs-revision": "f0fcfe0fdb22",
    "x-docs-manifest": "https://laso.finance/.well-known/docs-version.json",
    "contact": {
      "email": "agents+support@laso.finance"
    },
    "x-guidance": "Laso Finance is a payment-gated (x402) API that lets an AI agent spend USDC on real-world financial products: prepaid cards (U.S. and international), gift cards, push-to-card transfers to USD/EUR/GBP debit cards, and Venmo/PayPal payouts.\n\nPayment: every paid route is an x402 v2 endpoint. Call it with no payment header to receive a 402 challenge listing the accepted networks, then replay with a signed USDC payment. Both Base (eip155:8453) and Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) are accepted on every paid route; the caller picks either chain.\n\nIdentity: `GET /auth` is free and identity-only. Prove wallet ownership with a `SIGN-IN-WITH-X` (CAIP-122) header to receive a Firebase id_token, then send that token as a Bearer credential to the authenticated read routes (`get-card-data`, `get-account-balance`, `get-kyc-status`, etc.). Paid routes also return fresh auth credentials in their response, so a payment is never required just to obtain a token.\n\nRecommended flow: (1) `GET /auth` to establish identity, (2) call a paid route (e.g. `GET /get-card`) to purchase a product, paying USDC on Base or Solana, (3) poll the authenticated read routes with the returned Bearer token to fetch the resulting card/transfer details. Full machine-readable instructions live at https://laso.finance/SKILL.md.",
    "description": "Payment-gated API for Laso Finance. All paywalled routes use the x402 protocol — the caller includes a USDC payment header on Base (eip155:8453) or Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) and the server verifies payment before processing. Free routes require no payment header. The same 402 also carries an MPP (Machine Payments Protocol) challenge in WWW-Authenticate; an MPP client pays with USDC on Base by replaying with `Authorization: Payment ...`, and routes, prices, and responses are identical.\n\n## Getting started\n\nTo set up a wallet for making x402 payments, choose a provider:\n\n- **Locus** (default): https://paywithlocus.com/SKILL.md\n- **Sponge**: https://wallet.paysponge.com/skill.md — automatic x402 service discovery\n- **Ampersend**: https://www.ampersend.ai/getting-started.md — self-custody on Base or Solana with dual-approval spending limits. Laso Finance is a default skill, so no manual endpoint registration is needed.\n\n## How x402 works\n\n1. Call a paywalled endpoint without a payment header → receive a `402 Payment Required` response containing payment details (price, recipient address, network).\n2. Construct an x402 payment header using the details from the 402 response.\n3. Replay the request with the payment header → the server verifies payment and processes the request.\n\n## Authentication flow\n\n`GET /auth` is free: callers prove wallet ownership by sending a `SIGN-IN-WITH-X` header (CAIP-122 wallet signature). Paywalled routes (`/get-card`, `/order-gift-card`, `/get-push-to-card`, `/order-intl-card`) also return fresh auth credentials in their responses, so a payment is never required just to obtain a token.\n\nMost routes return auth credentials (`id_token`, `refresh_token`, `expires_in`). Use the `id_token` as a Bearer token to call authenticated Laso Finance endpoints like `/get-card-data`. When the `id_token` expires, use `POST /auth` with `grant_type: refresh_token` to get a new one.\n\n## Important notes\n\nThe `/get-card` USA prepaid card endpoint is U.S. only — issued in USD, usable at U.S.-based merchants only, and physical goods must ship to a U.S. address. For non-U.S. merchants or non-USD currencies, use `GET /order-intl-card` instead (international prepaid card, admin-fulfilled within 24 hours). All cards are intended for the caller's own use.\n\n## Rate limits\n\nEvery response carries the request budget so you can pace yourself without probing for a limit:\n\n| Header | Meaning |\n| --- | --- |\n| `RateLimit-Limit` | Requests permitted per window |\n| `RateLimit-Remaining` | Requests still available |\n| `RateLimit-Reset` | Seconds until the window rolls over |\n| `RateLimit-Policy` | The policy these numbers describe, as `limit;w=seconds` |\n\nThe same values are repeated as `X-RateLimit-*` for clients that only parse that spelling.\n\nMost routes advertise the service-wide ceiling. `POST /signup` enforces its own per-IP budget on top of it and overwrites these headers with its own numbers. `POST /refresh-card-data` is limited per card rather than per caller, so its headers keep the service-wide values and the per-card budget is reported only on rejection.\n\nEvery rejection is a `429` carrying `Retry-After` in seconds and a matching `retry_after_seconds` field in the body, computed from the limit that actually rejected the request. Wait that long and retry once. Do not retry in a tight loop.\n\nFor step-by-step instructions, read https://laso.finance/SKILL.md"
  },
  "x-service-info": {
    "categories": ["payments", "commerce", "fintech"],
    "docs": {
      "homepage": "https://laso.finance",
      "apiReference": "https://docs.laso.finance",
      "llms": "https://laso.finance/llms.txt"
    }
  },
  "servers": [
    {
      "url": "https://laso.finance",
      "description": "Production"
    }
  ],
  "paths": {
    "/version": {
      "get": {
        "operationId": "getVersion",
        "summary": "Get the current docs version",
        "description": "Returns the current content-hash version of the Laso Finance agent docs (`SKILL.md`, `openapi.json`, `llms.txt`, `llms-full.txt`, `ai-plugin.json`). Free, no auth or payment header.\n\nAgents aggressively cache these docs, so use this to detect a stale cache: if the `docs_version` you last saw differs from the value returned here, refetch the docs. Every API response also carries this value in the `X-Laso-Docs-Version` response header, alongside `X-Laso-Docs-Manifest`, which points at the authoritative freshness beacon at `https://laso.finance/.well-known/docs-version.json`.\n\nThis route is also the `status` target of the API catalog at `https://laso.finance/.well-known/api-catalog` (RFC 9727), which every response advertises via a `Link` header with `rel=\"api-catalog\"`.",
        "responses": {
          "200": {
            "description": "Current docs version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "docs_version": {
                      "type": "string",
                      "description": "12-character content hash of the agent docs. Changes only when the docs change."
                    },
                    "docs_manifest_url": {
                      "type": "string",
                      "description": "URL of the authoritative freshness beacon, served with no-cache."
                    },
                    "api_catalog_url": {
                      "type": "string",
                      "description": "URL of the RFC 9727 API catalog (`application/linkset+json`) indexing the OpenAPI description, the agent docs, and this status route."
                    },
                    "docs": {
                      "type": ["object", "null"],
                      "description": "Per-file entries, each with a `url` and a stable `sha256` content identifier. Compare against the values you last saw to tell which file changed."
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "docs_version": "8ea0eaadf372",
                  "docs_manifest_url": "https://laso.finance/.well-known/docs-version.json",
                  "api_catalog_url": "https://laso.finance/.well-known/api-catalog",
                  "docs": {
                    "SKILL.md": {
                      "url": "https://laso.finance/SKILL.md",
                      "sha256": "bb30ea2ee3cbd51b932f7e9fa1df35379ca589e203f23d1d51dd93dc0eb7a06c"
                    }
                  },
                  "note": "If your cached copy of the docs carries a different docs_version, refetch."
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth": {
      "get": {
        "operationId": "getAuth",
        "summary": "Get auth credentials",
        "description": "Free endpoint. Returns an ID token, refresh token, and user ID for the calling wallet. Use the ID token as a Bearer token to call Laso Finance APIs.\n\nProve wallet ownership by sending a `SIGN-IN-WITH-X` header: a base64-encoded CAIP-122 signed message. Build it with `@x402/extensions/sign-in-with-x` (e.g. `wrapFetchWithSIWx` handles the round-trip automatically). Works for any EVM (Base, eip155:8453) or Solana mainnet wallet.\n\nFor ID-token refresh use `POST /auth` with `grant_type=refresh_token`.",
        "parameters": [
          {
            "name": "SIGN-IN-WITH-X",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Base64-encoded CAIP-122 signed message proving wallet ownership."
          }
        ],
        "responses": {
          "200": {
            "description": "Auth credentials and user ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string",
                      "example": "https://laso.finance"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The user's ID (lowercase wallet address)"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "No valid payment or SIGN-IN-WITH-X proof was presented. All SIWX failures (missing or malformed header, invalid or expired signature, reused nonce, domain mismatch) return 402 per the x402 protocol. The response body is an empty JSON object; a fresh challenge (new nonce, payment options, SIWX info) is base64-encoded in the `PAYMENT-REQUIRED` response header. Sign the new challenge and retry. x402 client libraries such as `wrapFetchWithSIWx` handle this automatically. A **second** 402 on the paid retry means something different: the payment header verified but the transfer could not be settled on-chain. That response body is not empty and carries no `accepts` — it is the x402 settlement-failure shape `{\"success\": false, \"errorReason\": \"...\", \"errorMessage\": \"...\"}`, plus `x_laso_guidance` when a concrete next step applies. Distinguish the two by body: a challenge has `accepts`, a failure has `success: false`. The usual cause is an underfunded wallet, and the usual cause of that is the fee being charged **on top of** `amount` (a wallet holding exactly \\$2,000 cannot send a \\$2,000 payment). Nothing is charged for a failed settlement, so retrying with a smaller amount is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {}
                }
              }
            }
          }
        },
        "security": [
          {
            "siwx": []
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": "1000",
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Firebase ID token for the Laso callable APIs",
              "intent": "charge",
              "method": "evm"
            }
          ]
        }
      },
      "post": {
        "operationId": "refreshToken",
        "summary": "Refresh an ID token",
        "description": "Exchange a refresh token for a new ID token. This is a free endpoint — no x402 payment required. Uses the standard OAuth2 `grant_type=refresh_token` pattern.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["grant_type", "refresh_token"],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": ["refresh_token"],
                    "description": "Must be \"refresh_token\""
                  },
                  "refresh_token": {
                    "type": "string",
                    "description": "The refresh token received from a previous /auth or /get-card call"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refreshed auth credentials and user ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id_token": {
                      "type": "string",
                      "description": "New ID token — use as Bearer token for Laso Finance APIs"
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "New refresh token — use for the next refresh"
                    },
                    "expires_in": {
                      "type": "string",
                      "description": "Token lifetime in seconds"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The user's ID (lowercase wallet address)"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing grant_type or refresh_token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or missing grant_type. Must be \"refresh_token\" for POST /auth."
                }
              }
            }
          },
          "401": {
            "description": "Token is invalid or revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Failed to refresh token. Token may be invalid or revoked."
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/get-card": {
      "get": {
        "operationId": "orderCard",
        "summary": "Order a USA prepaid card",
        "description": "Pay between \\$5–\\$1000 USDC to order a Laso Finance USA prepaid card. By default returns JSON with auth credentials and card info (for AI agents). Pass `format=html` to get an HTML redirect to the card redemption URL (for browser-based flows).\n\nThe JSON response includes a `card.status` of `\"pending\"`. Card details (number, CVV, etc.) take ~7-10 seconds to become available. Poll `getCardData` at the `callableBaseUrl` to check when `status` becomes `\"ready\"`.\n\n**U.S. only.** This card is issued in USD and can only be used for purchases at U.S.-based merchants. Physical goods must ship to a U.S. address. For non-U.S. merchants or non-USD currencies, use `GET /order-intl-card` instead. This endpoint is intended for the caller's own use — the card is non-transferable.",
        "x-x402": [
          {
            "price": "dynamic",
            "priceRange": "$5–$1000",
            "scheme": "exact",
            "network": "eip155:8453",
            "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
          },
          {
            "price": "dynamic",
            "priceRange": "$5–$1000",
            "scheme": "exact",
            "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
            "payTo": "3MZVk97x9SeRxbYpc3jhzRfU2fyA3emYutnqfn9kNfYX"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": null,
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Prepaid card for use inside the United States",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "price": {
            "mode": "dynamic",
            "currency": "USD",
            "min": "5",
            "max": "1000"
          }
        },
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "USD amount to load on the card (min \\$5, max \\$1000)",
            "schema": {
              "type": "number",
              "minimum": 5,
              "maximum": 1000
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Response format. `json` (default) returns auth credentials and card info for agents. `html` returns a redirect page for browsers.",
            "schema": {
              "type": "string",
              "enum": ["json", "html"],
              "default": "json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Card order response. JSON by default, or HTML redirect if `format=html`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string",
                      "example": "https://laso.finance"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The user's ID (lowercase wallet address)"
                    },
                    "card": {
                      "$ref": "#/components/schemas/CardOrder"
                    }
                  }
                }
              },
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "HTML page with meta-refresh redirect to card redemption URL (when format=html)"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing amount",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Amount must be at least $5. Received: $2"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. No valid x402 payment header was included. The response body is an empty JSON object; the payment details (price, recipient address, network) are base64-encoded in the `PAYMENT-REQUIRED` response header. x402 client libraries handle this automatically. A **second** 402 on the paid retry means something different: the payment header verified but the transfer could not be settled on-chain. That response body is not empty and carries no `accepts` — it is the x402 settlement-failure shape `{\"success\": false, \"errorReason\": \"...\", \"errorMessage\": \"...\"}`, plus `x_laso_guidance` when a concrete next step applies. Distinguish the two by body: a challenge has `accepts`, a failure has `success: false`. The usual cause is an underfunded wallet, and the usual cause of that is the fee being charged **on top of** `amount` (a wallet holding exactly \\$2,000 cannot send a \\$2,000 payment). Nothing is charged for a failed settlement, so retrying with a smaller amount is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {}
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          }
        }
      }
    },
    "/get-card-data": {
      "get": {
        "operationId": "getCardData",
        "summary": "Get card details",
        "description": "Returns the current status and details of card orders. If `card_id` is provided, returns a single card, looked up across U.S. non-reloadable, international non-reloadable, and reloadable cards. If omitted, returns all cards of the given `card_type` for the authenticated user; `card_type` defaults to `Non-Reloadable U.S.` when omitted, so existing callers see unchanged behavior. Pass `card_type=Non-Reloadable International` to list international cards, or `card_type=Reloadable` to list reloadable cards.\n\nFor U.S. non-reloadable cards, details take ~7-10 seconds to become available after ordering; poll every 2-3 seconds until `status` is `\"ready\"`, then read `card_details`. For international non-reloadable cards, the order is `queued` until an admin fulfills it (typically within 24 hours), after which `card_details` is populated.\n\nFor international cards, the `card_id` returned by `/order-intl-card` is a queue id. After admin fulfillment, the issuer's transaction id becomes the new `card_id` and the original queue id is preserved on the card as `queued_order_card_id`. You can keep polling `/get-card-data?card_id=<original-queue-id>` and it will resolve to the fulfilled card.\n\n**Reloadable cards** are a separate product, set up by the account holder in the Laso dashboard rather than ordered through this API. They are reusable (a `multi_use` card stays open across charges until its limit is spent) and can be topped up, unlike the single-load non-reloadable cards. Listing them returns `balance`, `spend_limit`, `last4`, `expiry`, and `reusable`. If the wallet has no card issuer account linked, the list is empty and a `note` explains how the holder sets one up.\n\nReading a reloadable card's **number and CVV** is gated by the card issuer, and which gate applies depends on who issued it. Request the card by `card_id`; the response carries exactly one of three fields. `card_details` is the normal result for a card created through `/create-reloadable-card`, which Laso issues on the holder's behalf and can read for them directly — no approval step is involved. `details_approval` with `status: \"pending\"` and an `approval_id` appears only for a card the holder created in a DIFFERENT app: the issuer has emailed them an approve/deny link, and once they approve you retry as `GET /get-card-data?card_id=<CARD_ID>&approval_id=<APPROVAL_ID>` to receive `card_details`. `details_error` means the issuer could not return the number; retry shortly, and if it persists the holder can read the card in the dashboard. Spend the card by entering its number, expiry, and CVV at the merchant's checkout. `billing_address` is null on these cards and always will be — the issuer holds no billing address for a card. If a merchant requires one, use the address the account holder gave at identity verification (ask them; do not guess), since these cards are AVS-checked against it and a mismatch is the most common decline on a card that has funds. A card with `reusable: true` stays open after an approved charge and can be spent again up to its remaining `balance`; a charge larger than the balance is declined in full, as there are no partial approvals.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "card_id",
            "in": "query",
            "required": false,
            "description": "The card ID returned from `/get-card` or `/order-intl-card`, or a reloadable card's id from `card_type=Reloadable`. Looked up across all three card types. If omitted, returns all cards of `card_type`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "card_type",
            "in": "query",
            "required": false,
            "description": "When listing all cards (no `card_id`), filters by card type. Defaults to `Non-Reloadable U.S.` if omitted (preserves existing client behavior). Pass `Non-Reloadable International` to list international cards, or `Reloadable` to list reloadable cards.",
            "schema": {
              "type": "string",
              "enum": [
                "Non-Reloadable U.S.",
                "Non-Reloadable International",
                "Reloadable"
              ],
              "default": "Non-Reloadable U.S."
            }
          },
          {
            "name": "approval_id",
            "in": "query",
            "required": false,
            "description": "Reloadable cards only, and only for a card the account holder created in a DIFFERENT app: pass the approval_id from a prior details_approval response once they have approved it. Cards created through /create-reloadable-card are Laso-issued and never require this.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Card status and details. Returns a single `CardData` object when `card_id` is provided, or `{ \"cards\": CardData[] }` when omitted.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CardData"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "cards": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CardData"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid card_type. Must be \"Non-Reloadable\" or \"Non-Reloadable International\" when provided.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "card_type must be \"Non-Reloadable\" or \"Non-Reloadable International\" when provided. Received: foo"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          },
          "403": {
            "description": "Not authorized to view this card, or the account is frozen (frozen responses include a `frozen_message` field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Not authorized to view this card"
                }
              }
            }
          },
          "404": {
            "description": "Card not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Card not found"
                }
              }
            }
          }
        }
      }
    },
    "/list-card-transactions": {
      "get": {
        "operationId": "listCardTransactions",
        "summary": "List reloadable card transactions",
        "description": "Returns transactions on the authenticated user's **reloadable** cards, newest first: amount, merchant, status, and timestamps.\n\nPass `card_id` for one card's history, or omit it for every reloadable card on the account. Filter with `status` and cap the page with `limit`.\n\nThis route covers reloadable cards only. The two non-reloadable products keep their history in Laso's own records and are read through `/get-card-data`.\n\nIf the wallet has no card issuer account linked, the list is empty and a `note` explains how the account holder sets one up at https://laso.finance/agent/dashboard/verified/card.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "card_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Return only this card's transactions. Omit for every reloadable card on the account."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Maximum number of transactions to return. Defaults to the issuer's own page size (20)."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by status, e.g. `pending`, `settled`, `declined`, `reversed`, `refunded`."
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's reloadable card transactions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transactions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "transaction_id": {
                            "type": "string"
                          },
                          "card_id": {
                            "type": "string"
                          },
                          "merchant": {
                            "type": "string",
                            "description": "Merchant name, when the issuer reports one."
                          },
                          "amount": {
                            "type": "number",
                            "description": "Transaction amount in US dollars."
                          },
                          "status": {
                            "type": "string",
                            "description": "Lowercased status, e.g. `settled`."
                          },
                          "issuer_status": {
                            "type": "string",
                            "description": "The issuer's own status string, kept verbatim for support."
                          },
                          "created_at": {
                            "type": "number",
                            "description": "Milliseconds since the epoch."
                          }
                        }
                      }
                    },
                    "note": {
                      "type": "string",
                      "description": "Present only when no card issuer account is linked."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/get-card-deposit-address": {
      "get": {
        "operationId": "getCardDepositAddress",
        "summary": "Get the reloadable card deposit address",
        "description": "Returns the account holder's own USDC deposit address at the card issuer, and the balance currently on it. **This is how an agent funds a reloadable card balance with crypto.**\n\nFunding is user-funded: the address belongs to the account holder, so USDC sent to it credits their balance and funds their cards. The issuer provisions the address on first read, so simply calling this creates it.\n\n**Base only.** The address takes USDC on Base. USDC sent on another chain is unrecoverable. A Laso managed agent wallet holds USDC on **Solana**, so fund from a wallet that holds USDC on Base rather than transferring straight from a managed wallet.\n\nDeposits usually confirm within a couple of minutes. Poll this endpoint until `balance` reflects the deposit, then call `POST /create-reloadable-card`.\n\nThe address is stable per holder, but read it through this endpoint rather than caching it: the issuer is the only authority on it.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The holder's deposit address at the card issuer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "address": {
                      "type": "string",
                      "description": "The holder's own USDC receiving address at the card issuer."
                    },
                    "network": {
                      "type": "string",
                      "description": "The chain the address accepts. Always `base`."
                    },
                    "asset": {
                      "type": "string",
                      "description": "The asset the address accepts. Always `USDC`."
                    },
                    "balance": {
                      "type": "number",
                      "nullable": true,
                      "description": "Dollars currently on the card balance, or `null` when the issuer cannot read it (which is not the same as zero)."
                    },
                    "note": {
                      "type": "string",
                      "description": "Chain warning and the next step."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No card issuer account is linked to this wallet."
          }
        }
      }
    },
    "/get-saved-cards": {
      "get": {
        "operationId": "getSavedCards",
        "summary": "List the holder's saved cards",
        "description": "Lists the cards the account holder saved themselves: a card they already own, typed once on the card issuer's page and locked behind their passkey. **Nothing is issued or prefunded, and no identity verification is needed.** Every purchase paid with a saved card is approved by the holder on their own device.\n\nThe response carries display fields only, never a card number. An empty `cards` list means nothing is saved yet: call `POST /create-saved-card-link` and send the returned `url` to your human.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The holder's saved cards, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cards": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "card_id": {
                            "type": "string",
                            "description": "The saved card's id at the card issuer."
                          },
                          "last4": {
                            "type": "string",
                            "description": "The last four digits, for telling the account holder which card you mean."
                          },
                          "expiry_month": {
                            "type": "integer"
                          },
                          "expiry_year": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "integer",
                            "description": "When the holder saved it, in milliseconds since the epoch."
                          }
                        }
                      }
                    },
                    "blocker": {
                      "type": "string",
                      "enum": ["needs_code", "linked_elsewhere"],
                      "description": "Present only when the holder finished saving a card but it is not readable yet. `needs_code`: they saved it on a card account they already had, and confirm it once with a one-time code in the dashboard. `linked_elsewhere`: the card account they unlocked is linked to another Laso account. Relay `note`; sending another link does not help with `needs_code`."
                    },
                    "note": {
                      "type": "string",
                      "description": "The next step."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/create-saved-card-link": {
      "post": {
        "operationId": "createSavedCardLink",
        "summary": "Get a link for the holder to save their own card",
        "description": "Returns a single-use link where the account holder types a card they already own and locks it with their passkey. The card is encrypted on their device; Laso never sees the number.\n\n**Send the `url` to your human. Do not open it yourself.** Saving a card is the holder's step, like identity verification: the page asks for the card and a passkey, with no account to create and no code to type.\n\nPoll `GET /get-saved-cards` until the card appears. Each link works once and expires at `expires_at`; mint a new one if it lapses.\n\n**This route is free** and needs no identity verification or prior setup: the holder's first saved card creates their card account.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The link to send the account holder.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "The single-use page where the holder saves their card. Send it to them."
                    },
                    "expires_at": {
                      "type": "integer",
                      "description": "When the link stops working, in milliseconds since the epoch."
                    },
                    "note": {
                      "type": "string",
                      "description": "How to hand the link over and what to poll."
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/buy-with-saved-card": {
      "post": {
        "operationId": "buyWithSavedCard",
        "summary": "Shop and pay with the holder's saved card",
        "description": "Buys from online merchants (Amazon, Walmart, Target, Best Buy, DoorDash and more) and pays with the card the account holder saved themselves (see `GET /get-saved-cards`). One conversation per purchase:\n\n1. Send what to buy as `ask`, in plain language. Pass `delivery_address` on this first call when you know it.\n2. Relay `reply` to the holder and send their answers back as `ask` with the same `conversation_id`, until a `cart` comes back.\n3. Show the holder the cart. Once they agree, send its `hash` as `confirm` with the `conversation_id`. Nothing can be charged before this.\n4. The confirm answers `awaiting_approval` with an `approval_url`. **Send it to your human; never open it yourself.** They approve on their own device with their passkey. Then send the same confirm again to get `order_placed`.\n\nA turn runs against a live merchant and can take up to two minutes. This call waits about 45 seconds: a turn that finishes in time comes back complete (`state: done`); a slower one comes back as `state: running` with a `turn_id` to poll with `GET /get-purchase-turn`.\n\n**This route is free.** The holder's own card pays the merchant, and no identity verification is needed.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ask": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "What the holder wants, in plain language. Required unless confirming."
                  },
                  "conversation_id": {
                    "type": "string",
                    "description": "The conversation to continue. Required with `confirm`."
                  },
                  "confirm": {
                    "type": "string",
                    "pattern": "^[0-9a-f]{16}$",
                    "description": "A cart's `hash` from an earlier turn, to place exactly that cart."
                  },
                  "delivery_address": {
                    "type": "object",
                    "description": "Where to ship. Send it on the call that asks for the cart (a cart is bound to the address it was shown for) and on the confirm.",
                    "required": ["street", "city", "state", "zip"],
                    "properties": {
                      "street": {
                        "type": "string"
                      },
                      "address2": {
                        "type": "string"
                      },
                      "city": {
                        "type": "string"
                      },
                      "state": {
                        "type": "string",
                        "description": "Two-letter state or province code."
                      },
                      "zip": {
                        "type": "string",
                        "description": "US ZIP or Canadian postal code."
                      },
                      "phone": {
                        "type": "string",
                        "description": "Recipient phone. Retail shipping requires one."
                      },
                      "name": {
                        "type": "string",
                        "description": "Recipient name for the label."
                      },
                      "can_leave_at_door": {
                        "type": "boolean"
                      },
                      "country": {
                        "type": "string",
                        "description": "ISO 3166-1 alpha-2. Inferred from the postal code when omitted."
                      }
                    }
                  }
                }
              },
              "example": {
                "ask": "a 16 oz bag of Colombian ground coffee from Amazon",
                "delivery_address": {
                  "street": "1900 Jefferson St",
                  "city": "San Francisco",
                  "state": "CA",
                  "zip": "94123",
                  "phone": "+14155550100",
                  "name": "Ada Lovelace"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The turn, complete or still running.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "turn_id": {
                      "type": "string",
                      "description": "This turn's id. Poll `GET /get-purchase-turn` with it while `state` is `running`."
                    },
                    "state": {
                      "type": "string",
                      "enum": ["running", "done", "failed"],
                      "description": "`running` means the turn is still talking to the merchant; the fields below appear once it is `done`."
                    },
                    "created_at": {
                      "type": "integer",
                      "description": "Milliseconds since the epoch."
                    },
                    "conversation_id": {
                      "type": "string",
                      "description": "Send it back on every follow-up and on the confirm."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "needs_input",
                        "awaiting_approval",
                        "in_progress",
                        "order_placed",
                        "declined",
                        "conflict"
                      ],
                      "description": "What to do next. `needs_input`: a question or a cart waiting on a confirm. `awaiting_approval`: send `approval_url` to the holder, then repeat the same confirm. `in_progress`: the order is already being placed; read `GET /get-purchase-conversation` instead of confirming again. `order_placed`. `declined`: see `decline_code`. `conflict`: nothing ran, see `error_code` and confirm the fresh cart's hash. Never branch on `reply`."
                    },
                    "reply": {
                      "type": "string",
                      "description": "The shopping assistant's turn as prose, ready to show the holder."
                    },
                    "cart": {
                      "oneOf": [
                        {
                          "type": "object",
                          "properties": {
                            "merchant": {
                              "type": "string",
                              "description": "Merchant id, e.g. `retail` or `doordash`."
                            },
                            "merchant_name": {
                              "type": "string",
                              "description": "Display name, e.g. `Amazon`."
                            },
                            "items": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "name": {
                                    "type": "string"
                                  },
                                  "quantity": {
                                    "type": "integer"
                                  },
                                  "price_cents": {
                                    "type": ["integer", "null"]
                                  },
                                  "product_id": {
                                    "type": ["string", "null"],
                                    "description": "The merchant's id for the line, usually its product link."
                                  }
                                }
                              }
                            },
                            "fees_cents": {
                              "type": "integer",
                              "description": "All fees combined, including the service fee."
                            },
                            "tip_cents": {
                              "type": "integer"
                            },
                            "total_cents": {
                              "type": "integer",
                              "description": "The all-in total to show the account holder."
                            },
                            "approved_ceiling_cents": {
                              "type": ["integer", "null"],
                              "description": "The most the merchant may charge once final tax and shipping settle. This is what the holder's approval covers; any unused amount is released. Null when the merchant prices the cart exactly."
                            },
                            "hash": {
                              "type": "string",
                              "description": "Identity of exactly this cart. Send it back as `confirm` to place it."
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The most recently shown cart."
                    },
                    "carts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "merchant": {
                            "type": "string",
                            "description": "Merchant id, e.g. `retail` or `doordash`."
                          },
                          "merchant_name": {
                            "type": "string",
                            "description": "Display name, e.g. `Amazon`."
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "quantity": {
                                  "type": "integer"
                                },
                                "price_cents": {
                                  "type": ["integer", "null"]
                                },
                                "product_id": {
                                  "type": ["string", "null"],
                                  "description": "The merchant's id for the line, usually its product link."
                                }
                              }
                            }
                          },
                          "fees_cents": {
                            "type": "integer",
                            "description": "All fees combined, including the service fee."
                          },
                          "tip_cents": {
                            "type": "integer"
                          },
                          "total_cents": {
                            "type": "integer",
                            "description": "The all-in total to show the account holder."
                          },
                          "approved_ceiling_cents": {
                            "type": ["integer", "null"],
                            "description": "The most the merchant may charge once final tax and shipping settle. This is what the holder's approval covers; any unused amount is released. Null when the merchant prices the cart exactly."
                          },
                          "hash": {
                            "type": "string",
                            "description": "Identity of exactly this cart. Send it back as `confirm` to place it."
                          }
                        }
                      },
                      "description": "Every open cart in the conversation, oldest first."
                    },
                    "catalog": {
                      "type": "array",
                      "description": "The last product search. Empty on a turn that did not search again, so keep the previous one.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "product_id": {
                            "type": "string",
                            "description": "The merchant's product link."
                          },
                          "name": {
                            "type": ["string", "null"]
                          },
                          "price_cents": {
                            "type": ["integer", "null"]
                          },
                          "image_url": {
                            "type": ["string", "null"]
                          }
                        }
                      }
                    },
                    "unmatched": {
                      "type": "array",
                      "description": "Things asked for that did not make it into a cart, with a reason. Show these rather than guessing from `reply`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "merchant": {
                            "type": "string"
                          },
                          "requested": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string",
                            "enum": ["not_found", "unavailable"]
                          },
                          "detail": {
                            "type": ["string", "null"]
                          }
                        }
                      }
                    },
                    "order_id": {
                      "type": ["string", "null"],
                      "description": "Set when this turn placed an order."
                    },
                    "decline_code": {
                      "type": ["string", "null"],
                      "description": "The machine reason behind `declined`, e.g. `items_unavailable`."
                    },
                    "approval_url": {
                      "type": ["string", "null"],
                      "description": "Send to the holder when `status` is `awaiting_approval`. Never open it yourself."
                    },
                    "charge_status": {
                      "type": ["string", "null"],
                      "enum": [
                        "none",
                        "confirming",
                        "settled",
                        "unknown",
                        null
                      ],
                      "description": "Whether money moved: `none`, `confirming`, `settled`, or `unknown` (do not retry; read the conversation)."
                    },
                    "card_last4": {
                      "type": ["string", "null"],
                      "description": "The saved card that paid, for naming it to the holder."
                    },
                    "error_code": {
                      "type": ["string", "null"],
                      "description": "On `conflict`: `cart_changed`, `turn_in_progress`, `delivery_address_changed`, or `delivery_address_bound_after_cart`."
                    },
                    "error": {
                      "type": "string",
                      "description": "Present when `state` is `failed`."
                    },
                    "note": {
                      "type": "string",
                      "description": "The next step."
                    }
                  }
                },
                "example": {
                  "turn_id": "q8FvT2kLmN3pR5sX7yZa",
                  "state": "done",
                  "created_at": 1790366020000,
                  "conversation_id": "conv_49ec10bf959bba5559e67bac",
                  "status": "needs_input",
                  "reply": "Added it to your cart from Amazon. Total: $19.46. Let me know if you're ready to place it.",
                  "cart": {
                    "merchant": "retail",
                    "merchant_name": "Amazon",
                    "items": [
                      {
                        "name": "Cafe Quindio Medium Roast 100% Colombian Ground Coffee, 16 oz",
                        "quantity": 1,
                        "price_cents": 1899,
                        "product_id": "https://www.amazon.com/dp/B0C91LZ8PK"
                      }
                    ],
                    "fees_cents": 47,
                    "tip_cents": 0,
                    "total_cents": 1946,
                    "approved_ceiling_cents": 3073,
                    "hash": "5783c0c78f1c16a2"
                  },
                  "order_id": null,
                  "decline_code": null,
                  "approval_url": null,
                  "charge_status": null,
                  "card_last4": null,
                  "error_code": null,
                  "note": "Show the account holder the cart (items, total_cents, approved_ceiling_cents). Once they agree, send its hash as confirm with this conversation_id. To change it, send another ask instead."
                }
              }
            }
          },
          "400": {
            "description": "A malformed request (the `error` names the field), or no card is saved on this account yet. Call `POST /create-saved-card-link` and send the returned `url` to the account holder."
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        }
      }
    },
    "/get-purchase-turn": {
      "get": {
        "operationId": "getPurchaseTurn",
        "summary": "Read a purchase turn that was still running",
        "description": "Reads back a turn from `POST /buy-with-saved-card` that came back `state: running`. Poll every few seconds until `state` is `done` or `failed`; the response has the same shape as the POST.\n\nA `failed` confirm may still have placed the order. Read `GET /get-purchase-conversation` before sending it again.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "turn_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `turn_id` from `POST /buy-with-saved-card`."
          }
        ],
        "responses": {
          "200": {
            "description": "The turn.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "turn_id": {
                      "type": "string",
                      "description": "This turn's id. Poll `GET /get-purchase-turn` with it while `state` is `running`."
                    },
                    "state": {
                      "type": "string",
                      "enum": ["running", "done", "failed"],
                      "description": "`running` means the turn is still talking to the merchant; the fields below appear once it is `done`."
                    },
                    "created_at": {
                      "type": "integer",
                      "description": "Milliseconds since the epoch."
                    },
                    "conversation_id": {
                      "type": "string",
                      "description": "Send it back on every follow-up and on the confirm."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "needs_input",
                        "awaiting_approval",
                        "in_progress",
                        "order_placed",
                        "declined",
                        "conflict"
                      ],
                      "description": "What to do next. `needs_input`: a question or a cart waiting on a confirm. `awaiting_approval`: send `approval_url` to the holder, then repeat the same confirm. `in_progress`: the order is already being placed; read `GET /get-purchase-conversation` instead of confirming again. `order_placed`. `declined`: see `decline_code`. `conflict`: nothing ran, see `error_code` and confirm the fresh cart's hash. Never branch on `reply`."
                    },
                    "reply": {
                      "type": "string",
                      "description": "The shopping assistant's turn as prose, ready to show the holder."
                    },
                    "cart": {
                      "oneOf": [
                        {
                          "type": "object",
                          "properties": {
                            "merchant": {
                              "type": "string",
                              "description": "Merchant id, e.g. `retail` or `doordash`."
                            },
                            "merchant_name": {
                              "type": "string",
                              "description": "Display name, e.g. `Amazon`."
                            },
                            "items": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "name": {
                                    "type": "string"
                                  },
                                  "quantity": {
                                    "type": "integer"
                                  },
                                  "price_cents": {
                                    "type": ["integer", "null"]
                                  },
                                  "product_id": {
                                    "type": ["string", "null"],
                                    "description": "The merchant's id for the line, usually its product link."
                                  }
                                }
                              }
                            },
                            "fees_cents": {
                              "type": "integer",
                              "description": "All fees combined, including the service fee."
                            },
                            "tip_cents": {
                              "type": "integer"
                            },
                            "total_cents": {
                              "type": "integer",
                              "description": "The all-in total to show the account holder."
                            },
                            "approved_ceiling_cents": {
                              "type": ["integer", "null"],
                              "description": "The most the merchant may charge once final tax and shipping settle. This is what the holder's approval covers; any unused amount is released. Null when the merchant prices the cart exactly."
                            },
                            "hash": {
                              "type": "string",
                              "description": "Identity of exactly this cart. Send it back as `confirm` to place it."
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The most recently shown cart."
                    },
                    "carts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "merchant": {
                            "type": "string",
                            "description": "Merchant id, e.g. `retail` or `doordash`."
                          },
                          "merchant_name": {
                            "type": "string",
                            "description": "Display name, e.g. `Amazon`."
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "quantity": {
                                  "type": "integer"
                                },
                                "price_cents": {
                                  "type": ["integer", "null"]
                                },
                                "product_id": {
                                  "type": ["string", "null"],
                                  "description": "The merchant's id for the line, usually its product link."
                                }
                              }
                            }
                          },
                          "fees_cents": {
                            "type": "integer",
                            "description": "All fees combined, including the service fee."
                          },
                          "tip_cents": {
                            "type": "integer"
                          },
                          "total_cents": {
                            "type": "integer",
                            "description": "The all-in total to show the account holder."
                          },
                          "approved_ceiling_cents": {
                            "type": ["integer", "null"],
                            "description": "The most the merchant may charge once final tax and shipping settle. This is what the holder's approval covers; any unused amount is released. Null when the merchant prices the cart exactly."
                          },
                          "hash": {
                            "type": "string",
                            "description": "Identity of exactly this cart. Send it back as `confirm` to place it."
                          }
                        }
                      },
                      "description": "Every open cart in the conversation, oldest first."
                    },
                    "catalog": {
                      "type": "array",
                      "description": "The last product search. Empty on a turn that did not search again, so keep the previous one.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "product_id": {
                            "type": "string",
                            "description": "The merchant's product link."
                          },
                          "name": {
                            "type": ["string", "null"]
                          },
                          "price_cents": {
                            "type": ["integer", "null"]
                          },
                          "image_url": {
                            "type": ["string", "null"]
                          }
                        }
                      }
                    },
                    "unmatched": {
                      "type": "array",
                      "description": "Things asked for that did not make it into a cart, with a reason. Show these rather than guessing from `reply`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "merchant": {
                            "type": "string"
                          },
                          "requested": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string",
                            "enum": ["not_found", "unavailable"]
                          },
                          "detail": {
                            "type": ["string", "null"]
                          }
                        }
                      }
                    },
                    "order_id": {
                      "type": ["string", "null"],
                      "description": "Set when this turn placed an order."
                    },
                    "decline_code": {
                      "type": ["string", "null"],
                      "description": "The machine reason behind `declined`, e.g. `items_unavailable`."
                    },
                    "approval_url": {
                      "type": ["string", "null"],
                      "description": "Send to the holder when `status` is `awaiting_approval`. Never open it yourself."
                    },
                    "charge_status": {
                      "type": ["string", "null"],
                      "enum": [
                        "none",
                        "confirming",
                        "settled",
                        "unknown",
                        null
                      ],
                      "description": "Whether money moved: `none`, `confirming`, `settled`, or `unknown` (do not retry; read the conversation)."
                    },
                    "card_last4": {
                      "type": ["string", "null"],
                      "description": "The saved card that paid, for naming it to the holder."
                    },
                    "error_code": {
                      "type": ["string", "null"],
                      "description": "On `conflict`: `cart_changed`, `turn_in_progress`, `delivery_address_changed`, or `delivery_address_bound_after_cart`."
                    },
                    "error": {
                      "type": "string",
                      "description": "Present when `state` is `failed`."
                    },
                    "note": {
                      "type": "string",
                      "description": "The next step."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`turn_id` is missing, or no card is saved on this account yet."
          },
          "404": {
            "description": "No such turn on this account."
          }
        }
      }
    },
    "/get-purchase-conversation": {
      "get": {
        "operationId": "getPurchaseConversation",
        "summary": "Read a purchase conversation's orders",
        "description": "The record of a purchase conversation: whether a turn is still running, the last checkout attempt, and every order it placed with its charge status. Use it to track an order, and to settle what happened before repeating a confirm whose outcome is unclear (`in_progress`, `charge_status: unknown`, or a `failed` turn).",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "conversation_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `conversation_id` from a purchase turn."
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation_id": {
                      "type": "string"
                    },
                    "open": {
                      "type": "boolean"
                    },
                    "turn_in_progress": {
                      "type": "boolean",
                      "description": "True while a turn is still running. Wait for it to clear before reading `orders`."
                    },
                    "last_checkout": {
                      "type": ["object", "null"],
                      "properties": {
                        "merchant": {
                          "type": "string"
                        },
                        "merchant_name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "placed",
                            "pending",
                            "denied",
                            "needs_approval",
                            "error"
                          ]
                        },
                        "message": {
                          "type": ["string", "null"]
                        },
                        "order_id": {
                          "type": ["string", "null"]
                        },
                        "decline_code": {
                          "type": ["string", "null"]
                        },
                        "approval_url": {
                          "type": ["string", "null"]
                        },
                        "charge_status": {
                          "type": ["string", "null"]
                        },
                        "total_cents": {
                          "type": ["integer", "null"]
                        },
                        "at": {
                          "type": "integer",
                          "description": "Milliseconds since the epoch."
                        }
                      }
                    },
                    "orders": {
                      "type": "array",
                      "description": "Every order this conversation placed, oldest first.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "order_id": {
                            "type": "string"
                          },
                          "merchant": {
                            "type": "string"
                          },
                          "merchant_name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "settled",
                              "confirming",
                              "cancelled",
                              "failed"
                            ]
                          },
                          "total_cents": {
                            "type": "integer",
                            "description": "The all-in amount, fees included."
                          },
                          "card_last4": {
                            "type": ["string", "null"]
                          },
                          "placed_at": {
                            "type": ["integer", "null"],
                            "description": "Milliseconds since the epoch."
                          }
                        }
                      }
                    },
                    "carts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "merchant": {
                            "type": "string",
                            "description": "Merchant id, e.g. `retail` or `doordash`."
                          },
                          "merchant_name": {
                            "type": "string",
                            "description": "Display name, e.g. `Amazon`."
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "quantity": {
                                  "type": "integer"
                                },
                                "price_cents": {
                                  "type": ["integer", "null"]
                                },
                                "product_id": {
                                  "type": ["string", "null"],
                                  "description": "The merchant's id for the line, usually its product link."
                                }
                              }
                            }
                          },
                          "fees_cents": {
                            "type": "integer",
                            "description": "All fees combined, including the service fee."
                          },
                          "tip_cents": {
                            "type": "integer"
                          },
                          "total_cents": {
                            "type": "integer",
                            "description": "The all-in total to show the account holder."
                          },
                          "approved_ceiling_cents": {
                            "type": ["integer", "null"],
                            "description": "The most the merchant may charge once final tax and shipping settle. This is what the holder's approval covers; any unused amount is released. Null when the merchant prices the cart exactly."
                          },
                          "hash": {
                            "type": "string",
                            "description": "Identity of exactly this cart. Send it back as `confirm` to place it."
                          }
                        }
                      },
                      "description": "Carts still open in the conversation."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`conversation_id` is missing, or no card is saved on this account yet."
          },
          "404": {
            "description": "No such conversation on this account."
          }
        }
      }
    },
    "/create-reloadable-card": {
      "post": {
        "operationId": "createReloadableCard",
        "summary": "Create a reloadable card",
        "description": "Creates a **reloadable** card funded from the account holder's balance with the card issuer, and returns it. This is the same card the holder creates in the Laso dashboard, so an agent and a human end up with the same product.\n\nThe card is **reusable by default** (`reusable: true`): it stays open across charges until its limit is spent. Pass `reusable: false` for a single-use card that closes after its first approved charge.\n\n**This route is free** — no x402 payment. The card draws on the account holder's own balance with the issuer, so Laso fronts nothing.\n\n**If the balance is short**, this route answers `402`. Fund it by sending USDC: `GET /get-card-deposit-address` returns the holder's own deposit address at the card issuer (USDC on Base only), and the balance credits within a couple of minutes. The holder can also top up with Apple or Google Pay in the dashboard.\n\nRead the new card's number and CVV with `/get-card-data?card_id=...`. A card created here is Laso-issued, so its `card_details` come back directly — the approval step applies only to cards the holder created in a different app.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["usd_amount"],
                "properties": {
                  "usd_amount": {
                    "type": "number",
                    "description": "Amount in US dollars to load onto the card, minimum \\$1. Sent as dollars, not cents."
                  },
                  "reusable": {
                    "type": "boolean",
                    "description": "Whether the card stays open across charges. Defaults to `true`; `false` closes it after the first approved charge."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The newly created card.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "card": {
                      "type": "object",
                      "properties": {
                        "card_id": {
                          "type": "string"
                        },
                        "card_type": {
                          "type": "string"
                        },
                        "last4": {
                          "type": "string"
                        },
                        "expiry": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "balance": {
                          "type": "number",
                          "description": "Card balance in US dollars."
                        },
                        "spend_limit": {
                          "type": "number"
                        },
                        "reusable": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "402": {
            "description": "The account holder's balance with the issuer is too low to fund the card. Fund it via GET /get-card-deposit-address and retry."
          },
          "400": {
            "description": "Invalid `usd_amount`, or no card issuer account is linked to this wallet."
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/get-account-balance": {
      "get": {
        "operationId": "getAccountBalance",
        "summary": "Get account balance",
        "description": "Returns the current account balance and total deposits for the authenticated user.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Account balance information",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountBalance"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "404": {
            "description": "Account balance not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Account balance not found"
                }
              }
            }
          }
        }
      }
    },
    "/get-kyc-status": {
      "get": {
        "operationId": "getKycStatus",
        "summary": "Get KYC verification status (optional)",
        "tags": ["Verification (optional)"],
        "description": "**KYC is optional.** Most endpoints (cards, gift cards, push-to-card, account balance, withdrawals) do not require verification. It is only needed for certain features such as Venmo/PayPal payouts via `/send-payment`, and may be used for additional controls in the future. If you are not using those features, you can ignore the verification endpoints entirely.\n\nReturns the calling wallet's cached KYC verification status. Use this as a free pre-flight check before paying for `/send-payment`: if `kyc_verified` is `true` the payout will go through. If it is `false`, don't call `/send-payment` yet — that call would not send the payout (it returns `kyc_required` and a `kyc_url`), and the USDC you paid would just land in your account balance, recoverable with `POST /withdraw`. To start verification when not verified, call `/get-kyc-link`.\n\nThis reads the cached status kept up to date by the verification webhook; it does not start verification or return a verification link.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "KYC verification status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "kyc_verified": {
                      "type": "boolean",
                      "description": "Whether the wallet has completed identity verification. When true, /send-payment dispatches the payout. When false, /send-payment does not send: it returns kyc_required and the USDC paid lands in account balance (recoverable via POST /withdraw). Verify before calling /send-payment."
                    },
                    "kyc_review_status": {
                      "type": "string",
                      "nullable": true,
                      "description": "Latest review status from the verification provider, or null if never reviewed."
                    },
                    "kyc_review_answer": {
                      "type": "string",
                      "nullable": true,
                      "description": "Latest review answer (e.g. GREEN for approved, RED for rejected), or null if never reviewed."
                    },
                    "kyc_last_reviewed_at": {
                      "type": "number",
                      "nullable": true,
                      "description": "Epoch milliseconds of the last review, or null if never reviewed."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          }
        }
      }
    },
    "/get-kyc-link": {
      "get": {
        "operationId": "getKycLink",
        "summary": "Get a KYC verification link (optional)",
        "tags": ["Verification (optional)"],
        "description": "**KYC is optional.** It is only needed for certain features such as Venmo/PayPal payouts via `/send-payment`, and may be used for additional controls in the future. If you are not using those features, you can ignore the verification endpoints.\n\nReturns a one-time identity-verification link (`kyc_url`) for the calling wallet. Open it (or hand it to the wallet owner) to complete verification. The typical flow is: call `/get-kyc-status`, and only if `kyc_verified` is `false` call this to get a link, complete it, then proceed to `/send-payment`.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Verification link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "kyc_url": {
                      "type": "string",
                      "description": "One-time URL to open to complete identity verification."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          }
        }
      }
    },
    "/register-webhook": {
      "post": {
        "operationId": "registerWebhook",
        "summary": "Register a notification webhook (free)",
        "tags": ["Notifications"],
        "description": "Registers (or replaces) an HTTPS webhook URL to receive the calling wallet's account notifications as signed POSTs — banking application status changes, bank transfer and payout completions, agent wallet deposits, card orders, and every other event the user is notified about. This closes the polling gap for agents: instead of re-fetching status endpoints, point this at any URL you can receive HTTP on (your harness's inbound webhook endpoint, or a relay you poll).\n\nDeliveries are signed per the Standard Webhooks specification (https://www.standardwebhooks.com/): each POST carries `webhook-id`, `webhook-timestamp`, and `webhook-signature` (`v1,<base64 HMAC-SHA256>`) headers verifiable with any standard-webhooks library using the returned `secret`. The body is `{\"type\": \"notification.<category>\", \"timestamp\": \"<ISO 8601>\", \"data\": {\"user_id\", \"title\", \"text\", \"category\"}}`.\n\nThe `secret` is returned exactly once, by this call. Re-registering rotates the secret, replaces the URL, and re-enables a registration that was auto-disabled after 50 consecutive failed deliveries. Registering also notifies the account owner through their other channels and fires a first signed test delivery (`type` `notification.account`) at the new URL.\n\nDeliveries time out after 10 seconds and are not retried; treat the webhook as a low-latency hint and the status endpoints as the source of truth. The URL must be public HTTPS.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Public HTTPS URL to receive signed notification POSTs. Max 512 characters. Private/internal hosts are rejected."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "registered": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    },
                    "secret": {
                      "type": "string",
                      "description": "Standard Webhooks signing secret (`whsec_...`). Shown only in this response — store it now. Rotate by re-registering."
                    },
                    "signing": {
                      "type": "string",
                      "description": "Always `standard-webhooks`."
                    }
                  }
                },
                "example": {
                  "registered": true,
                  "url": "https://agent.example.com/hooks/laso",
                  "secret": "whsec_EXAMPLEONLYnotarealsecretAAAAAAA",
                  "signing": "standard-webhooks"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "400": {
            "description": "Missing or invalid `url` (not HTTPS, not public, too long, or malformed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "url must use https"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                }
              }
            }
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/get-webhook": {
      "get": {
        "operationId": "getWebhook",
        "summary": "Get webhook registration & delivery health (free)",
        "tags": ["Notifications"],
        "description": "Returns the calling wallet's webhook registration and its delivery health. The signing secret is never returned here — it is shown once by `POST /register-webhook`; re-register to rotate it.\n\nA registration is auto-disabled after 50 consecutive failed deliveries (`disabled` becomes `true`); re-register to re-enable it.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Registration state (`registered: false` when none exists)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "registered": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "nullable": true
                    },
                    "disabled": {
                      "type": "boolean",
                      "description": "True when auto-disabled after repeated delivery failures. Re-register to re-enable."
                    },
                    "disabled_reason": {
                      "type": "string",
                      "nullable": true
                    },
                    "consecutive_failures": {
                      "type": "number"
                    },
                    "last_delivery_status": {
                      "type": "string",
                      "nullable": true,
                      "description": "`delivered` or `failed`, or null before the first delivery."
                    },
                    "last_delivery_timestamp": {
                      "type": "number",
                      "nullable": true,
                      "description": "Epoch milliseconds of the last delivery attempt."
                    },
                    "last_delivery_detail": {
                      "type": "string",
                      "nullable": true,
                      "description": "`HTTP <status>` of the last attempt, or the error message."
                    },
                    "last_delivery_message_id": {
                      "type": "string",
                      "nullable": true,
                      "description": "The `webhook-id` header of the last delivery."
                    }
                  }
                },
                "example": {
                  "registered": true,
                  "url": "https://agent.example.com/hooks/laso",
                  "disabled": false,
                  "disabled_reason": null,
                  "consecutive_failures": 0,
                  "last_delivery_status": "delivered",
                  "last_delivery_timestamp": 1753900000000,
                  "last_delivery_detail": "HTTP 200",
                  "last_delivery_message_id": "msg_a1b2c3d4e5f6a7b8c9d0e1f2"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                }
              }
            }
          }
        }
      }
    },
    "/delete-webhook": {
      "post": {
        "operationId": "deleteWebhook",
        "summary": "Delete the notification webhook (free)",
        "tags": ["Notifications"],
        "description": "Removes the calling wallet's webhook registration. Notifications keep flowing to the user's other channels (push, SMS, Telegram, in-app); only the webhook deliveries stop. The account owner is notified of the removal.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "description": "False when there was no registration to delete."
                    }
                  }
                },
                "example": {
                  "deleted": true
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                }
              }
            }
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/withdraw": {
      "post": {
        "operationId": "withdraw",
        "summary": "Withdraw from account balance",
        "description": "Initiate a withdrawal from the user's account balance. The USDC is sent on Solana to the `solana_address` you supply; the destination is never inferred, so the address is required.\n\nThis is an asynchronous operation. A successful call answers `202 Accepted`: the withdrawal is created in a `pending` state and sent within a few minutes. Poll the returned `status_url` (`GET /get-withdrawal-status?withdrawal_id=...`) until `state` reaches a terminal value rather than treating the 202 as completion.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["amount", "solana_address"],
                "properties": {
                  "amount": {
                    "type": "number",
                    "minimum": 0.01,
                    "description": "Amount to withdraw in USD"
                  },
                  "solana_address": {
                    "type": "string",
                    "description": "Solana wallet address that receives the USDC. Must be a valid base58 Solana address."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Withdrawal accepted. The withdrawal is recorded in a `pending` state and the USDC is sent on Solana within a few minutes, so this response reports that the request was accepted, not that it settled. Poll `status_url` (also returned in the `Location` header) until `withdrawal.state` reaches a terminal value.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "status_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Absolute URL to poll for this withdrawal's state. Always present, including on an idempotent replay, so prefer it over the `Location` header.",
                      "example": "https://laso.finance/get-withdrawal-status?withdrawal_id=abc123"
                    },
                    "withdrawal": {
                      "$ref": "#/components/schemas/WithdrawalRecord"
                    }
                  }
                }
              }
            },
            "headers": {
              "Location": {
                "description": "URL of the status resource for this withdrawal, the same value as `status_url`. Not set on an idempotent replay.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "400": {
            "description": "Invalid amount, missing or invalid `solana_address` (`code: invalid_solana_address`), or insufficient funds",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient funds. Current balance: $12.50"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "404": {
            "description": "Account balance not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Account balance not found"
                }
              }
            }
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/get-withdrawal-status": {
      "get": {
        "operationId": "getWithdrawalStatus",
        "summary": "Get withdrawal statuses",
        "description": "Returns the status of withdrawals for the authenticated user. If `withdrawal_id` is provided, returns a single withdrawal. If omitted, returns all withdrawals.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "withdrawal_id",
            "in": "query",
            "required": false,
            "description": "Get a specific withdrawal by ID. If omitted, returns all withdrawals.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawal status. Returns `{ \"withdrawal\": WithdrawalStatus }` when `withdrawal_id` is provided, or `{ \"withdrawals\": WithdrawalStatus[] }` when omitted.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "withdrawal": {
                          "$ref": "#/components/schemas/WithdrawalStatus"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "withdrawals": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/WithdrawalStatus"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "404": {
            "description": "Withdrawal not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Withdrawal not found"
                }
              }
            }
          }
        }
      }
    },
    "/refresh-card-data": {
      "post": {
        "operationId": "refreshCardData",
        "summary": "Trigger a card data refresh",
        "description": "Requests an updated balance for a card.\n\nFor U.S. non-reloadable cards (default), the card is re-scraped from the issuer via a retrieval queue and processed asynchronously. Rate limited per card to one request every 5 minutes, and to at most 12 refreshes in any rolling 24-hour period.\n\nFor international non-reloadable cards, a balance update request is recorded; an admin will manually update the balance within 24 hours. While a balance update is already pending for a card, additional requests for that card return 409.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["card_id"],
                "properties": {
                  "card_id": {
                    "type": "string",
                    "description": "The card ID to refresh data for"
                  },
                  "card_type": {
                    "type": "string",
                    "description": "The type of card. Defaults to `Non-Reloadable U.S.` if omitted.",
                    "enum": [
                      "Non-Reloadable U.S.",
                      "Non-Reloadable International"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Card refresh requested successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Card refresh requested."
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "400": {
            "description": "Missing card_id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "card_id is required in request body"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "404": {
            "description": "Card not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Card not found"
                }
              }
            }
          },
          "409": {
            "description": "Card already has a pending refresh/balance update request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A balance update is already pending for this card."
                }
              }
            }
          },
          "429": {
            "description": "Rate limited — either less than 5 minutes since the last refresh of this card, or this card has reached its 24-refresh daily limit. Both limits are per card. `Retry-After` and `retry_after_seconds` give the exact wait.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Human-readable description of the limit that was hit."
                    },
                    "code": {
                      "type": "string",
                      "const": "rate_limited",
                      "description": "Stable code to branch on."
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "description": "Seconds to wait before retrying. Matches the `Retry-After` header."
                    },
                    "hint": {
                      "type": "string",
                      "description": "What to do differently."
                    }
                  }
                },
                "example": {
                  "error": "Daily card refresh limit reached. You can request at most 12 refreshes for a card in a 24-hour period. Please try again later.",
                  "code": "rate_limited",
                  "retry_after_seconds": 3142,
                  "hint": "This limit is per card: at most one refresh every 5 minutes, and 12 in any rolling 24-hour period. Wait retry_after_seconds and try again, or read the cached balance with GET /get-card-data in the meantime."
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/order-intl-card": {
      "get": {
        "operationId": "orderIntlCard",
        "summary": "Order an international prepaid card",
        "description": "Order an international non-reloadable prepaid card (USD). The order is queued and fulfilled by an admin — after fulfillment, card details become available via `GET /get-card-data` with `card_type=Non-Reloadable International`.\n\nThe x402 payment covers the on-card amount plus a 3.8% fee, or a lower rate while a limited-time fee promotion is running (it can be 0%). The 402 challenge always carries the exact total. Minimum \\$100, maximum \\$1,000 on-card amount. The \\$1,000 maximum is the amount loaded on the card and does not change with the fee rate. Submit a whole dollar amount.\n\nPlacing the order accepts the card issuer's terms on behalf of the account holder: the [Cardholder Agreement](https://laso.finance/intl-card-cardholder-agreement.pdf), the [E-Communications Disclosure](https://laso.finance/intl-card-e-communications-disclosure.pdf), and the [Privacy Notice](https://laso.finance/intl-card-privacy-notice.pdf). \n\nIf you need to cancel a queued order before it is fulfilled, use `POST /cancel-intl-order`.",
        "x-x402": [
          {
            "price": "dynamic",
            "priceRange": "$100–$1,038 USD (on-card amount \\$100–\\$1,000 + 3.8% fee, lower during a fee promotion)",
            "scheme": "exact",
            "network": "eip155:8453",
            "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
          },
          {
            "price": "dynamic",
            "priceRange": "$100–$1,038 USD (on-card amount \\$100–\\$1,000 + 3.8% fee, lower during a fee promotion)",
            "scheme": "exact",
            "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
            "payTo": "3MZVk97x9SeRxbYpc3jhzRfU2fyA3emYutnqfn9kNfYX"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": null,
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "International prepaid card",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "price": {
            "mode": "dynamic",
            "currency": "USD",
            "min": "100",
            "max": "1038"
          }
        },
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "On-card USD amount (min \\$100, max \\$1,000 regardless of the fee rate). Must be a whole dollar amount: the issuer only issues whole-dollar cards. The x402 payment is this amount plus a 3.8% fee, or less during a fee promotion.",
            "schema": {
              "type": "number",
              "minimum": 100,
              "maximum": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "International card order queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string"
                    },
                    "user_id": {
                      "type": "string"
                    },
                    "intl_card_order": {
                      "type": "object",
                      "properties": {
                        "on_card_usd_amount": {
                          "type": "number"
                        },
                        "charged_usd_amount": {
                          "type": "number"
                        },
                        "status": {
                          "type": "string",
                          "example": "queued"
                        },
                        "timestamp": {
                          "type": "number"
                        }
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid amount",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Amount must be at least $100. Received: $50"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. No valid x402 payment header was included. The response body is an empty JSON object; the payment details (price, recipient address, network) are base64-encoded in the `PAYMENT-REQUIRED` response header. x402 client libraries handle this automatically. A **second** 402 on the paid retry means something different: the payment header verified but the transfer could not be settled on-chain. That response body is not empty and carries no `accepts` — it is the x402 settlement-failure shape `{\"success\": false, \"errorReason\": \"...\", \"errorMessage\": \"...\"}`, plus `x_laso_guidance` when a concrete next step applies. Distinguish the two by body: a challenge has `accepts`, a failure has `success: false`. The usual cause is an underfunded wallet, and the usual cause of that is the fee being charged **on top of** `amount` (a wallet holding exactly \\$2,000 cannot send a \\$2,000 payment). Nothing is charged for a failed settlement, so retrying with a smaller amount is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {}
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "500": {
            "description": "Order failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Failed to order international card. Please contact support."
                }
              }
            }
          }
        }
      }
    },
    "/cancel-intl-order": {
      "post": {
        "operationId": "cancelIntlOrder",
        "summary": "Cancel a queued international card order",
        "description": "Cancel a previously queued international non-reloadable card order, as long as it has not yet been fulfilled by an admin (`state=queued`). The charged amount is credited back to the account balance.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["card_id"],
                "properties": {
                  "card_id": {
                    "type": "string",
                    "description": "The card ID of the queued order to cancel (returned from `/order-intl-card`)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order cancelled and charged amount refunded to account balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "card_id": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "400": {
            "description": "Missing card_id, or card is not in a queued state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "card_id is required in request body"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "404": {
            "description": "Card not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Card not found"
                }
              }
            }
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/search-merchants": {
      "get": {
        "operationId": "searchMerchants",
        "summary": "Search merchant spend data",
        "description": "Search Laso's merchant database for confirmed spend data for a given card type. Returns whether the card was accepted, not accepted, or unknown at each merchant.\n\nUse `card_type` to search by the USA prepaid card (`Non-Reloadable U.S.`, the default) or the international prepaid card (`Non-Reloadable International`). USA searches exclude merchants with non-US country-code TLDs; international searches do not.\n\n**Important:** This database only contains merchants where Laso users have previously attempted a transaction. A merchant not being listed, or being listed as `unknown`, does NOT mean the card won't work there — it just means it hasn't been tried yet. If a merchant is listed as `accepted`, you can confidently use the card there. If listed as `not_accepted`, the card will fail at that merchant.\n\nBanking and money-transfer merchants are always returned as `not_accepted` with a `restriction_note`, whatever the transaction history shows. Spending there counts as a transfer of value and is not supported on any card.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query — the merchant name to search for (e.g. \"amazon\", \"netflix\")",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "card_type",
            "in": "query",
            "required": false,
            "description": "Which card type to search acceptance for. Defaults to `Non-Reloadable U.S.` if omitted (preserves existing client behavior). Pass `Non-Reloadable International` to search for international prepaid card acceptance.",
            "schema": {
              "type": "string",
              "enum": ["Non-Reloadable U.S.", "Non-Reloadable International"],
              "default": "Non-Reloadable U.S."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "merchants": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MerchantResult"
                      }
                    },
                    "query": {
                      "type": "string",
                      "description": "The search query that was used"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of merchants returned"
                    },
                    "card_type": {
                      "type": "string",
                      "description": "The card type results are filtered for",
                      "example": "Non-Reloadable U.S."
                    },
                    "note": {
                      "type": "string",
                      "description": "Important caveat about the data — this database only includes merchants where users have previously attempted transactions"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing search query",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "q query parameter is required"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          }
        }
      }
    },
    "/signup": {
      "post": {
        "operationId": "startSignup",
        "summary": "Start a signup for a human with no Laso account",
        "description": "Begins account creation for a human who does not have a Laso account yet. Requires no authentication, because the caller has no account.\n\nThis does **not** create the account. An agent cannot: the account is created by Privy in the human's own browser, so it belongs to them and not to their agent. What this returns is a `claim_url` to hand over.\n\nGive your human the `claim_url` verbatim in your very next reply (send `message_for_human` if unsure what to say), and do not open it yourself. Then poll `GET /signup-status`, which holds the connection open until they finish and hands you your `lasoak_` API key.\n\nRate limited per IP.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_name": {
                    "type": "string",
                    "description": "Your name, shown to your human on the claim screen and recorded as the connected agent.",
                    "example": "Ada"
                  },
                  "label": {
                    "type": "string",
                    "description": "Optional label recorded on the minted API key so your human can tell keys apart.",
                    "example": "ada-prod"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Signup started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "claim_url": {
                      "type": "string",
                      "description": "Give this to your human verbatim. Never open it yourself.",
                      "example": "https://laso.finance/agent/dashboard/claim?code=K7MPQ-W3XZ9"
                    },
                    "claim_code": {
                      "type": "string",
                      "description": "The code inside claim_url. Pass it back to /signup-status alongside claim_token.",
                      "example": "K7MPQ-W3XZ9"
                    },
                    "claim_token": {
                      "type": "string",
                      "description": "Your receipt for this signup. Shown once; store it. Exchanged at /signup-status for your API key.",
                      "example": "lasoclaim_..."
                    },
                    "expires_at": {
                      "type": "number",
                      "description": "Epoch ms when the claim link expires."
                    },
                    "expires_in": {
                      "type": "number",
                      "description": "Seconds until the claim link expires (86400).",
                      "example": 86400
                    },
                    "message_for_human": {
                      "type": "string",
                      "description": "A ready-to-send reply containing the link. Send this as your next message."
                    },
                    "next_steps": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many signups from this IP address. `Retry-After` and `retry_after_seconds` give the exact wait.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Human-readable description of the limit that was hit."
                    },
                    "code": {
                      "type": "string",
                      "const": "rate_limited",
                      "description": "Stable code to branch on."
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "description": "Seconds to wait before retrying. Matches the `Retry-After` header."
                    },
                    "hint": {
                      "type": "string",
                      "description": "What to do differently."
                    }
                  }
                },
                "example": {
                  "error": "Too many signups from this address. Try again later.",
                  "code": "rate_limited",
                  "retry_after_seconds": 1800,
                  "hint": "Signups are limited per IP address, with a short burst window on top of an hourly cap. Wait retry_after_seconds and try once more. If you already started a signup, poll GET /signup-status with its claim_token instead of starting another."
                }
              }
            }
          }
        }
      }
    },
    "/signup-status": {
      "get": {
        "operationId": "getSignupStatus",
        "summary": "Wait for your human to finish the signup",
        "description": "Long-polls until the human opens the claim link and finishes creating the account. Holds the connection open for about 60 seconds and returns **204 No Content** if they have not finished yet.\n\n**Retry on 204 and nothing else.** The call already blocks for you, so add no sleep between attempts. Every other status is final and retrying will not change the answer: 401 means the token is wrong, 410 means the link expired. Both carry `\"terminal\": true` in the body. A loop written as \"retry until 200\" spins at network speed against those, because they answer instantly rather than holding the connection.\n\nOn completion it returns your `api_key` — **delivered exactly once**. Store it before doing anything else; a later poll returns the account without it. By that point the human is on their dashboard, their agent wallet is provisioned, and you are already recorded as their connected agent, so `announceAgentConnection` is not needed on this path.",
        "parameters": [
          {
            "name": "claim_token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The claim_token from POST /signup."
          },
          {
            "name": "claim_code",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The claim_code from the same POST /signup response."
          }
        ],
        "responses": {
          "200": {
            "description": "The human completed the signup",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "claimed"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The Laso user id of the new account.",
                      "example": "usr_1a2b3c..."
                    },
                    "api_key": {
                      "type": "string",
                      "nullable": true,
                      "description": "Your lasoak_ key. Delivered EXACTLY ONCE — null on any later poll.",
                      "example": "lasoak_..."
                    },
                    "api_key_note": {
                      "type": "string",
                      "description": "Present only when api_key is null, explaining that it was already delivered."
                    },
                    "auth": {
                      "type": "object",
                      "description": "An id_token / refresh_token bundle you can use immediately."
                    },
                    "callable_base_url": {
                      "type": "string"
                    },
                    "next_steps": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "204": {
            "description": "Not claimed yet. Call again — this is the only status you should retry."
          },
          "401": {
            "description": "Unknown or invalid claim token. Terminal: do not retry."
          },
          "410": {
            "description": "The claim link expired before anyone opened it. Terminal: do not retry — call POST /signup for a fresh one."
          }
        }
      }
    },
    "/get-auth-link": {
      "get": {
        "operationId": "getAuthLink",
        "summary": "Get a login link for the web dashboard",
        "description": "Returns a URL that a human can open in a browser to log in to the Laso Finance web dashboard as the authenticated user. Useful for humans who want to see what their AI agent has been doing (view cards, transactions, balances, etc.).\n\nThe URL carries a short single-use login code rather than a long token, so relay it to your human verbatim; it passes through tool-output credential filters intact. Do not fetch or open the URL yourself: redeeming the code consumes the human's login.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Auth link generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth_url": {
                      "type": "string",
                      "description": "URL to open in a browser to log in to the Laso Finance dashboard. Carries a short single-use login code.",
                      "example": "https://laso.finance/agent/dashboard/auth?code=K7MPQ-W3XZ9"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The authenticated user's ID (wallet address)",
                      "example": "0xabc..."
                    },
                    "expires_in": {
                      "type": "number",
                      "description": "Seconds until the login code expires",
                      "example": 900
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          }
        }
      }
    },
    "/feedback": {
      "post": {
        "operationId": "sendFeedback",
        "summary": "Send feedback about the API",
        "description": "Tell the humans running Laso what worked, what was confusing, and what you wish existed. It lands on their dashboard, so it is the best channel for API friction and feature requests.\n\nSend the `id_token` from `/auth` or `/get-card` as a Bearer token.\n\nRequires at least one completed real action (a settled deposit, purchase, or withdrawal); at most 5 entries per 24 hours.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["feedback"],
                "properties": {
                  "feedback": {
                    "type": "string",
                    "description": "The main free-text feedback. Required. Sanitized and capped server-side (max 2000 characters).",
                    "example": "order-intl-card was smooth but I wanted a way to see the fee before paying"
                  },
                  "what_they_want": {
                    "type": "string",
                    "description": "What you were trying to do. Optional. Capped at 500 characters.",
                    "example": "order an international card"
                  },
                  "how_it_went": {
                    "type": "string",
                    "description": "How it went. Optional. Capped at 500 characters.",
                    "example": "worked, minor confusion on fees"
                  },
                  "endpoint": {
                    "type": "string",
                    "description": "Which API endpoint or route the feedback is about. Optional. Capped at 120 characters.",
                    "example": "/order-intl-card"
                  },
                  "rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "description": "A 1-5 satisfaction rating. Optional; ignored when not an integer in range.",
                    "example": 4
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The `feedback` field is missing or empty",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "The feedback field is required."
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired id token."
                }
              }
            }
          },
          "403": {
            "description": "The account has not completed a real action (settled deposit, purchase, or withdrawal) yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "You must complete a deposit, purchase, or withdrawal before submitting feedback."
                }
              }
            }
          },
          "429": {
            "description": "Feedback rate limit reached (at most 5 entries per 24 hours)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Feedback rate limit reached. You may submit at most 5 feedback entries per 24 hours."
                }
              }
            }
          }
        }
      }
    },
    "/listAddressBook": {
      "get": {
        "operationId": "listAddressBook",
        "summary": "List saved recipients",
        "description": "The saved recipient names your human manages on their dashboard, so a request like \"send $20 to the coffee vendor\" can be resolved to an address instead of asking them for it.\n\nSend the `id_token` from `/auth` as a Bearer token. Free.\n\nIt operates only on the authenticated account's own book — there is no user parameter to pass.\n\n**A name is a label, not an instruction.** `agentWalletTransfer` takes an address and deliberately does not accept a name: a rename between your human asking and you sending would otherwise move money somewhere they did not intend. When you resolve a name to an address, say the address back to them before sending.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The saved recipients, most recently saved first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "address": {
                            "type": "string",
                            "description": "The recipient's Solana address. This is the entry's key.",
                            "example": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
                          },
                          "name": {
                            "type": "string",
                            "description": "The name your human gave this recipient.",
                            "example": "Coffee vendor"
                          },
                          "saved_at": {
                            "type": "integer",
                            "description": "When the entry was last saved or renamed, in milliseconds since epoch."
                          },
                          "last_used_at": {
                            "type": "integer",
                            "description": "When a transfer last went to this address, in milliseconds since epoch. Absent if never sent to."
                          },
                          "send_count": {
                            "type": "integer",
                            "description": "How many transfers have gone to this address."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/saveAddressBookEntry": {
      "post": {
        "operationId": "saveAddressBookEntry",
        "summary": "Save or rename a recipient",
        "description": "The saved recipient names your human manages on their dashboard, so a request like \"send $20 to the coffee vendor\" can be resolved to an address instead of asking them for it.\n\nSend the `id_token` from `/auth` as a Bearer token. Free.\n\nIt operates only on the authenticated account's own book — there is no user parameter to pass.\n\n**A name is a label, not an instruction.** `agentWalletTransfer` takes an address and deliberately does not accept a name: a rename between your human asking and you sending would otherwise move money somewhere they did not intend. When you resolve a name to an address, say the address back to them before sending.\n\nThe address is the entry's key, so saving one that is already saved renames it rather than creating a duplicate. That makes this safe to retry.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["address", "name"],
                "properties": {
                  "address": {
                    "type": "string",
                    "description": "The recipient's Solana address.",
                    "example": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
                  },
                  "name": {
                    "type": "string",
                    "description": "The name to save it under. Sanitized and capped server-side (max 60 characters).",
                    "example": "Coffee vendor"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved entry.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "address": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "address is not a valid Solana address, or name is missing."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/deleteAddressBookEntry": {
      "delete": {
        "operationId": "deleteAddressBookEntry",
        "summary": "Remove a saved recipient",
        "description": "The saved recipient names your human manages on their dashboard, so a request like \"send $20 to the coffee vendor\" can be resolved to an address instead of asking them for it.\n\nSend the `id_token` from `/auth` as a Bearer token. Free.\n\nIt operates only on the authenticated account's own book — there is no user parameter to pass.\n\n**A name is a label, not an instruction.** `agentWalletTransfer` takes an address and deliberately does not accept a name: a rename between your human asking and you sending would otherwise move money somewhere they did not intend. When you resolve a name to an address, say the address back to them before sending.\n\nRemoving an entry only forgets the name; it has no effect on past transfers to that address.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "address",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The address to remove. May be sent here or in the JSON body."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "address": {
                    "type": "string",
                    "description": "The address to remove."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The removed address.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "address": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "address is required."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/get-push-to-card": {
      "get": {
        "operationId": "pushToCard",
        "summary": "Send money to a debit card (USD, EUR, GBP)",
        "description": "Pay USDC to initiate a push-to-card transfer. Specify the face value in the target currency (USD, EUR, or GBP); Laso computes the USDC cost using the current exchange rate plus a 4.8% fee (minimum fee of \\$1.50/€1.50/£1.50 in the transfer currency), and that becomes the x402 price. Returns a `redemption_url` that must be opened to complete the transfer. The form requires sender name, debit card number, and cardholder name.\n\n**Supported currencies:**\n- `USD` — U.S. debit cards (U.S. bank accounts only)\n- `EUR` — Eurozone debit cards\n- `GBP` — U.K. debit cards\n\nLaso cannot perform the transfer directly — the redemption URL must be visited and the debit card details entered manually (by the agent or a human).\n\n**Fee:** 4.8%, with a minimum of 1.50 in the transfer currency (included in the USDC price, computed from the requested face value).",
        "x-x402": [
          {
            "price": "dynamic",
            "priceRange": "~$11.50–$10,000 USD (face value 10–9,541.98 in USD/EUR/GBP + 4.8% fee with a 1.50 minimum, converted to USD)",
            "scheme": "exact",
            "network": "eip155:8453",
            "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
          },
          {
            "price": "dynamic",
            "priceRange": "~$11.50–$10,000 USD (face value 10–9,541.98 in USD/EUR/GBP + 4.8% fee with a 1.50 minimum, converted to USD)",
            "scheme": "exact",
            "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
            "payTo": "3MZVk97x9SeRxbYpc3jhzRfU2fyA3emYutnqfn9kNfYX"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": null,
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Payout to a debit card in USD, EUR or GBP",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "price": {
            "mode": "dynamic",
            "currency": "USD",
            "min": "11.50",
            "max": "10000"
          }
        },
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "Face value to send to the debit card, denominated in the chosen `currency` (min 10, max 9,541.98). The x402 payment price is this face value converted to USD plus a 4.8% fee (minimum fee of 1.50 in the chosen currency).",
            "schema": {
              "type": "number",
              "minimum": 10,
              "maximum": 9541.98
            }
          },
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "description": "Target currency for the debit card transfer. Defaults to `USD`.",
            "schema": {
              "type": "string",
              "enum": ["USD", "EUR", "GBP"],
              "default": "USD"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Push-to-card transfer initiated. Open the redemption_url to enter debit card details.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string",
                      "example": "https://laso.finance"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The user's ID (lowercase wallet address)"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "example": "Push-to-card transfer initiated. Open the redemption_url to enter your debit card details and complete the transfer."
                    },
                    "amount": {
                      "type": "number",
                      "description": "The face value sent to the debit card, in the requested `currency`"
                    },
                    "currency": {
                      "type": "string",
                      "enum": ["USD", "EUR", "GBP"],
                      "description": "The target currency of the debit card transfer"
                    },
                    "redemption_url": {
                      "type": "string",
                      "description": "URL to open and complete the push-to-card transfer. The form requires: sender name, debit card number, and cardholder name."
                    },
                    "note": {
                      "type": "string",
                      "description": "Important information about the currency's regional restriction and required form fields"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing amount, or no payer address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "currency must be USD, EUR, or GBP. Received: XYZ"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. No valid x402 payment header was included. The response body is an empty JSON object; the payment details (price, recipient address, network) are base64-encoded in the `PAYMENT-REQUIRED` response header. x402 client libraries handle this automatically. A **second** 402 on the paid retry means something different: the payment header verified but the transfer could not be settled on-chain. That response body is not empty and carries no `accepts` — it is the x402 settlement-failure shape `{\"success\": false, \"errorReason\": \"...\", \"errorMessage\": \"...\"}`, plus `x_laso_guidance` when a concrete next step applies. Distinguish the two by body: a challenge has `accepts`, a failure has `success: false`. The usual cause is an underfunded wallet, and the usual cause of that is the fee being charged **on top of** `amount` (a wallet holding exactly \\$2,000 cannot send a \\$2,000 payment). Nothing is charged for a failed settlement, so retrying with a smaller amount is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {}
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          }
        }
      }
    },
    "/send-payment": {
      "get": {
        "operationId": "sendPayment",
        "summary": "Send a payment via Venmo or PayPal",
        "description": "Pay USDC to send money to a Venmo or PayPal recipient. The x402 USDC price is the requested amount plus a 4.9% fee (with a \\$1.50 minimum fee). On-chain payment credits the calling wallet's Laso account balance via the standard deposit webhook; the callable then debits the gross amount and dispatches the payout.\n\n**Asynchronous.** A dispatched payout answers `202 Accepted`, not `200`: the payment is recorded and handed to the payout provider, which settles it afterwards. Poll the returned `status_url` (`GET /get-payment-status?payment_id=...`) until `state` reaches a terminal value rather than treating the 202 as money received. A `200` means the opposite: nothing was dispatched because identity verification is required.\n\n**KYC required.** The first time a wallet sends a Venmo or PayPal payout it must complete identity verification. If the wallet is not yet verified the response returns `kyc_required: true` and a `kyc_url` — open the URL, complete the flow, and retry. If you don't want to proceed, the credited account balance can be withdrawn with `POST /withdraw`.\n\n**Recipient details.** For Venmo, `recipient_id` is the recipient's 10-digit U.S. phone number, and the first and last name are optional. No email is needed. For PayPal, `recipient_id` is the recipient's PayPal email, which also serves as `recipient_email` (so `recipient_email` is optional), and the first and last name are required.\n\n**Fee:** 4.9% with a \\$1.50 minimum (included in the USDC price).",
        "x-x402": [
          {
            "price": "dynamic",
            "priceRange": "~$6.50–$1,049 USD (amount $5–$1,000 × 1.049 fee, min $1.50 fee)",
            "scheme": "exact",
            "network": "eip155:8453",
            "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
          },
          {
            "price": "dynamic",
            "priceRange": "~$6.50–$1,049 USD (amount $5–$1,000 × 1.049 fee, min $1.50 fee)",
            "scheme": "exact",
            "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
            "payTo": "3MZVk97x9SeRxbYpc3jhzRfU2fyA3emYutnqfn9kNfYX"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": null,
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Payout via Venmo or PayPal",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "price": {
            "mode": "dynamic",
            "currency": "USD",
            "min": "6.50",
            "max": "1049"
          }
        },
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "USD amount to send to the recipient (min \\$5, max \\$1,000). The x402 payment price is this amount plus a 4.9% fee (with a \\$1.50 minimum).",
            "schema": {
              "type": "number",
              "minimum": 5,
              "maximum": 1000
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": true,
            "description": "Payment platform.",
            "schema": {
              "type": "string",
              "enum": ["venmo", "paypal"]
            }
          },
          {
            "name": "recipient_id",
            "in": "query",
            "required": true,
            "description": "Recipient identifier. For Venmo: 10-digit U.S. phone number. For PayPal: email address.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient_first_name",
            "in": "query",
            "required": false,
            "description": "Recipient's first name (English letters A-Z and a-z only). Required for PayPal, optional for Venmo.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient_last_name",
            "in": "query",
            "required": false,
            "description": "Recipient's last name (English letters A-Z and a-z only). Required for PayPal, optional for Venmo.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient_email",
            "in": "query",
            "required": false,
            "description": "PayPal only. Recipient's email address; defaults to recipient_id (the PayPal email). Ignored for Venmo.",
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Identity verification is required, so no payout was dispatched. The x402 payment you made was still credited to your Laso account balance: complete the flow at `kyc_url` and retry, or call `POST /withdraw` to pull the USDC back to your wallet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string"
                    },
                    "user_id": {
                      "type": "string"
                    },
                    "kyc_required": {
                      "type": "boolean",
                      "enum": [true]
                    },
                    "kyc_url": {
                      "type": "string",
                      "description": "URL the wallet owner must open to complete identity verification. No payout was sent, but the x402 payment you made was still credited to your Laso account balance. After verifying, retry /send-payment to dispatch the payout, or call POST /withdraw to pull the credited USDC back to your wallet."
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable explanation of the KYC requirement and how to recover the credited funds via retry or POST /withdraw."
                    },
                    "platform": {
                      "type": "string",
                      "enum": ["venmo", "paypal"]
                    },
                    "amount": {
                      "type": "number"
                    },
                    "recipient_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Payout accepted. The payment is recorded and dispatched to the payout provider, which settles it afterwards, so this response reports that the request was accepted, not that the recipient has the money. Poll `status_url` (also returned in the `Location` header) until `state` reaches a terminal value.",
            "headers": {
              "Location": {
                "description": "URL of the status resource for this payout, the same value as `status_url`.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string"
                    },
                    "user_id": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "platform": {
                      "type": "string",
                      "enum": ["venmo", "paypal"]
                    },
                    "amount": {
                      "type": "number"
                    },
                    "recipient_id": {
                      "type": "string"
                    },
                    "payment_id": {
                      "type": "string",
                      "description": "Id of the payout record just created. Pass it to `GET /get-payment-status?payment_id=...` to follow this specific payout."
                    },
                    "status_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Absolute URL to poll for this payout's state. Always present, so prefer it over the `Location` header.",
                      "example": "https://laso.finance/get-payment-status?payment_id=abc123"
                    },
                    "state": {
                      "type": "string",
                      "description": "Payment lifecycle state (e.g. `in-process`, `queued`)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameters, or no payer address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Payment required — response body contains x402 payment details. A **second** 402 on the paid retry means something different: the payment header verified but the transfer could not be settled on-chain. That response body is not empty and carries no `accepts` — it is the x402 settlement-failure shape `{\"success\": false, \"errorReason\": \"...\", \"errorMessage\": \"...\"}`, plus `x_laso_guidance` when a concrete next step applies. Distinguish the two by body: a challenge has `accepts`, a failure has `success: false`. The usual cause is an underfunded wallet, and the usual cause of that is the fee being charged **on top of** `amount` (a wallet holding exactly \\$2,000 cannot send a \\$2,000 payment). Nothing is charged for a failed settlement, so retrying with a smaller amount is safe."
          }
        }
      }
    },
    "/fund-card-balance": {
      "get": {
        "operationId": "fundCardBalance",
        "summary": "Load a reloadable card balance",
        "description": "Pay USDC to load the account holder's reloadable card balance at the card issuer.\n\n**Why this exists:** the card issuer only accepts USDC on **Base**, and a Laso managed agent wallet holds USDC on **Solana**. Pay this route from either chain and Laso bridges the payment to your own deposit address at the issuer over Circle's CCTP, which burns on the source chain and mints native USDC on Base.\n\n**Price:** exactly the amount you are loading, and exactly that amount is credited to the balance. Loading \\$50 costs \\$50 and puts \\$50 on the card. Laso covers Circle's bridging fee by burning slightly more than requested.\n\n**A linked card issuer account is required.** The account holder sets it up at https://laso.finance/agent/dashboard/verified/card.\n\n**Delivery starts once your payment settles.** The response confirms the payment and returns a `top_up_id`; the transfer to your card balance follows and usually lands in under a minute. A payment made on Base is already on the issuer's chain, so it is forwarded without a bridge. Watch the top-up under `top_ups` in `GET /list-card-transactions`, where `status` moves `paid` → `bridging` → `delivered` → `credited` and the transaction hash behind each leg appears as it lands, or poll `GET /get-card-deposit-address` until `balance` reflects it.\n\n**Follow with `POST /create-reloadable-card`** to create a card against the balance once it has arrived.\n\n**If the top-up's status becomes `failed`**, nothing is stranded: the USDC you paid has credited your Laso account balance. Retry, or recover it with `POST /withdraw`.",
        "x-x402": [
          {
            "price": "dynamic",
            "priceRange": "$5–$1,000 USD (the amount loaded; no fee added)",
            "scheme": "exact",
            "network": "eip155:8453",
            "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
          },
          {
            "price": "dynamic",
            "priceRange": "$5–$1,000 USD (the amount loaded; no fee added)",
            "scheme": "exact",
            "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
            "payTo": "3MZVk97x9SeRxbYpc3jhzRfU2fyA3emYutnqfn9kNfYX"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": null,
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Top-up of a reloadable card balance",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "price": {
            "mode": "dynamic",
            "currency": "USD",
            "min": "5",
            "max": "1000"
          }
        },
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "USD of USDC to load onto the card balance (min \\$5, max \\$1,000). The x402 price equals this amount; Laso absorbs the bridge cost.",
            "schema": {
              "type": "number",
              "minimum": 5,
              "maximum": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payment settled; delivery to the card balance has started.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "top_up_id": {
                      "type": "string",
                      "description": "Identifies this top-up under `top_ups` in `GET /list-card-transactions`."
                    },
                    "status": {
                      "type": "string",
                      "example": "paid",
                      "description": "Always `paid` here: the payment settled and delivery to the card balance has started. Follow the later states in `GET /list-card-transactions`."
                    },
                    "funded_usd": {
                      "type": "number",
                      "description": "USD loaded onto the card balance."
                    },
                    "deposit_address": {
                      "type": "string",
                      "description": "The account holder's own deposit address at the card issuer, on Base."
                    },
                    "network": {
                      "type": "string",
                      "example": "base"
                    },
                    "asset": {
                      "type": "string",
                      "example": "USDC"
                    },
                    "payment_network": {
                      "type": "string",
                      "example": "solana",
                      "description": "The chain the payment settled on: `solana` (bridged to Base over CCTP) or `base` (forwarded directly)."
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No card issuer account is linked to this wallet, or the amount is out of range."
          },
          "402": {
            "description": "Payment required (x402 challenge), or the payment was not settled."
          },
          "500": {
            "description": "The funding failed after payment. The USDC credited your Laso account balance."
          }
        }
      }
    },
    "/withdraw-card-balance": {
      "post": {
        "operationId": "withdrawCardBalance",
        "summary": "Withdraw from a reloadable card balance",
        "description": "Move unspent money off the account holder's reloadable card. The balance behind a reloadable card is held by the card issuer, and this asks the issuer to pay `amount` of it out as USDC on **Base** to the `destination_address` you supply. It is the reverse of `GET /fund-card-balance`, and free: the money is the holder's own, and Laso moves nothing itself.\n\n**Base only.** The issuer pays on Base and nowhere else, so the destination must be a Base wallet you control. A Laso managed agent wallet holds USDC on **Solana** and cannot receive this. The card deposit address from `GET /get-card-deposit-address` is not a valid destination either.\n\n**Not instant.** The issuer processes payouts manually, usually within 1-3 business days, and emails the account holder when the payout is sent. The response carries the issuer's `withdrawal_id`; there is no status route to poll. The request is recorded as a `withdrawal` event under `card_events` in `GET /list-card-transactions`, and the balance from `GET /get-card-deposit-address` drops once it is paid.\n\n**Amount:** \\$2 to \\$10,000, and no more than the balance the issuer can release. Withdrawals the issuer has not paid yet still count against the balance, so a second request can only take what remains.\n\nConfirm the amount and destination with the account holder before calling this. A linked card issuer account is required; the holder sets it up at https://laso.finance/agent/dashboard/verified/card.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "tags": ["cards"],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["amount", "destination_address"],
                "properties": {
                  "amount": {
                    "type": "number",
                    "minimum": 2,
                    "maximum": 10000,
                    "description": "USD to withdraw from the card balance (min \\$2, max \\$10,000)."
                  },
                  "destination_address": {
                    "type": "string",
                    "description": "The 0x-prefixed Base wallet address that receives the USDC. Must be one you control; Base only."
                  }
                }
              },
              "example": {
                "amount": 25,
                "destination_address": "0x1234567890abcdef1234567890abcdef12345678"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The issuer accepted the withdrawal request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "withdrawal_id": {
                      "type": "string",
                      "description": "The card issuer's reference for this payout request. Quote it to support; it also appears on the matching `withdrawal` card event."
                    },
                    "status": {
                      "type": "string",
                      "example": "requested",
                      "description": "Always `requested`: the issuer has accepted the request and will pay it out manually."
                    },
                    "amount": {
                      "type": "number",
                      "description": "USD the issuer will pay out."
                    },
                    "destination_address": {
                      "type": "string",
                      "description": "The Base address the USDC goes to."
                    },
                    "network": {
                      "type": "string",
                      "example": "base"
                    },
                    "asset": {
                      "type": "string",
                      "example": "USDC"
                    },
                    "note": {
                      "type": "string",
                      "description": "Timing and where to see the request afterwards."
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "400": {
            "description": "Invalid `amount` (`amount_below_minimum`, `amount_above_maximum`), an invalid or non-Base `destination_address` (`invalid_destination_address`), no linked card issuer account, or the issuer refused the request: `insufficient_card_balance` (the `error` names how much can be withdrawn right now), `withdrawals_disabled`, `withdrawals_unavailable`, or `withdrawal_rejected`. The `error` carries the issuer's own reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "The user can withdraw at most $18.50 right now (open requests count against the balance). Ask them to pick an amount within that.",
                  "code": "insufficient_card_balance",
                  "hint": "Read the current balance with GET /get-card-deposit-address and request that amount or less. Withdrawals the issuer has not paid yet still count against it."
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/payment-recipients": {
      "get": {
        "operationId": "listPaymentRecipients",
        "summary": "List saved Venmo and PayPal recipients (free)",
        "description": "Lists the Venmo or PayPal recipients this account has paid before, so you can resolve a name your human said (\"send $20 to Jane\") into the `handle` that `GET /send-payment` takes as its `recipient_id`. Free — only the payout itself is a paid action.\n\nTwo different ids are in play. Each entry's `recipient_id` is an opaque saved-entry id: pass it to `POST` and `DELETE /payment-recipients`. Its `handle` is the phone number or email you actually pay: pass that as `recipient_id` to `GET /send-payment`. Passing the saved-entry id to `GET /send-payment` does not send to the saved person.\n\nUse `total_sent` and `last_sent_timestamp` to disambiguate similar names, and confirm with your human rather than guessing between two entries.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "required": true,
            "description": "Payment platform whose saved recipients to list.",
            "schema": {
              "type": "string",
              "enum": ["venmo", "paypal"]
            }
          },
          {
            "name": "include_archived",
            "in": "query",
            "required": false,
            "description": "Include archived entries. Defaults to false.",
            "schema": {
              "type": "string",
              "enum": ["true", "false"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The saved recipients for that platform.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "platform": {
                      "type": "string",
                      "enum": ["venmo", "paypal"]
                    },
                    "recipients": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "recipient_id": {
                            "type": "string",
                            "description": "Opaque id of this saved entry (for example `aQ3xK9mZpL2vB7nR4tYw`). Pass it as `recipient_id` to `POST` and `DELETE /payment-recipients`. It is not what `GET /send-payment` takes; use `handle` there."
                          },
                          "handle": {
                            "type": "string",
                            "description": "What to pay them at: a 10-digit phone number for Venmo, an email address for PayPal. Pass this as `recipient_id` to `GET /send-payment`."
                          },
                          "name": {
                            "type": "string",
                            "description": "The recipient's own name on the platform."
                          },
                          "display_name": {
                            "type": "string",
                            "description": "The label the account owner chose. Prefer it when confirming a payment back to a human."
                          },
                          "status": {
                            "type": "string"
                          },
                          "total_sent": {
                            "type": "number",
                            "description": "Total dollars sent to this recipient from this account."
                          },
                          "send_count": {
                            "type": "integer"
                          },
                          "last_sent_timestamp": {
                            "type": "integer",
                            "description": "Milliseconds since the epoch."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "updatePaymentRecipient",
        "summary": "Rename or archive a saved recipient (free)",
        "description": "Renames a saved Venmo or PayPal recipient, archives it, or both. Provide at least one of `display_name` or `archived`.\n\nArchiving hides an entry from the default list while keeping its history, which is the reversible way to retire a recipient. Prefer it over `DELETE`.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["platform", "recipient_id"],
                "properties": {
                  "platform": {
                    "description": "Payment platform the recipient is saved under.",
                    "type": "string",
                    "enum": ["venmo", "paypal"]
                  },
                  "recipient_id": {
                    "type": "string",
                    "description": "The entry's `recipient_id` from `GET /payment-recipients` (an opaque id, not the phone number or email)."
                  },
                  "display_name": {
                    "type": "string",
                    "description": "Must be a string. A wrong type returns a terminal 400 with code `invalid_display_name`."
                  },
                  "archived": {
                    "type": "boolean",
                    "description": "Must be a boolean. A wrong type returns a terminal 400 with code `invalid_archived`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated recipient.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "platform": {
                      "type": "string"
                    },
                    "recipient_id": {
                      "type": "string"
                    },
                    "display_name": {
                      "type": "string"
                    },
                    "archived": {
                      "type": "boolean"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/Idempotency-Replayed"
              }
            }
          },
          "409": {
            "description": "A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A request with this Idempotency-Key is still being processed",
                  "code": "idempotency_key_in_progress"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Idempotency-Key reused with a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "delete": {
        "operationId": "deletePaymentRecipient",
        "summary": "Remove a saved recipient (free)",
        "description": "Removes a saved Venmo or PayPal recipient. Payment history is kept; only the saved entry stops being offered.\n\nThis is not reversible through the API, so prefer archiving (`POST /payment-recipients` with `archived: true`) when the account owner may want the recipient back, and confirm before removing someone they did not ask you to remove.\n\n`platform` and `recipient_id` may be sent in the JSON body or the query string. Requires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Required unless sent in the body.",
            "schema": {
              "type": "string",
              "enum": ["venmo", "paypal"]
            }
          },
          {
            "name": "recipient_id",
            "in": "query",
            "required": false,
            "description": "Required unless sent in the body. The entry's `recipient_id` from `GET /payment-recipients` (an opaque id, not the phone number or email).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The recipient was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "recipient_id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/get-payment-status": {
      "get": {
        "operationId": "getPaymentStatus",
        "summary": "Check Venmo and PayPal payout status (free)",
        "description": "Reports the state of the Venmo and PayPal payouts this account has sent with `GET /send-payment`. Free: only the payout itself is a paid action.\n\nThree ways to call it: with `payment_id` to fetch one payout, with `recipient_id` (from `GET /payment-recipients`) to list every payout to that saved recipient, or with neither to list every payout this account has sent, newest first.\n\n`state` is `queued` (waiting on account balance), `in-process` (dispatched to the platform), `complete`, `failed`, or `cancelled`. `queued` and `in-process` are the two states still in flight; poll every few minutes, or register a webhook with `POST /register-webhook` to be told when a payout completes instead. A payout that has been `in-process` for more than an hour is worth raising with the account owner. `state` is absent on payouts sent before state tracking existed.\n\nBank payouts are not listed here: follow those with `listBankingTransactions`. Push-to-card transfers are completed by the recipient in the browser and have no server-side state.\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "payment_id",
            "in": "query",
            "required": false,
            "description": "Fetch one payout by its `id`. Takes precedence over `recipient_id`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient_id",
            "in": "query",
            "required": false,
            "description": "List every payout to one saved recipient. This is the `recipient_id` returned by `GET /payment-recipients`, not the phone number or email.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns `{ \"payment\": PaymentStatus }` when `payment_id` is provided, or `{ \"payments\": PaymentStatus[] }` (newest first) otherwise.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "payment": {
                          "$ref": "#/components/schemas/PaymentStatus"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "payments": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PaymentStatus"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "single": {
                    "summary": "One payout by payment_id",
                    "value": {
                      "payment": {
                        "id": "otlUYhH8n7Q2KWmt6FBs",
                        "amount_pre_fees": 10,
                        "amount_with_fees": 11.5,
                        "fees_paid": 1.5,
                        "platform": "venmo",
                        "state": "complete",
                        "state_updated_at": 1789365621369,
                        "recipient_id": "Bh3bHdQwErTyUiOpAsDf",
                        "recipient_first_name": "Jane",
                        "recipient_last_name": "Doe",
                        "timestamp": 1789364854261,
                        "timestamp_readable": "9/14/2026, 6:47:34 AM"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or expired token"
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                }
              }
            }
          },
          "404": {
            "description": "`payment_id` was given and no Venmo or PayPal payout with that id belongs to this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Payment not found",
                  "code": "payment_not_found",
                  "hint": "No Venmo or PayPal payment with that id belongs to this account. Use the id from GET /get-payment-status without a payment_id, which lists every payout this account has sent."
                }
              }
            }
          }
        }
      }
    },
    "/send-bank-payment": {
      "get": {
        "operationId": "sendBankPayment",
        "summary": "Send dollars to a bank account",
        "description": "Pay USDC to send dollars to a bank account by ACH. The x402 USDC price is the requested amount plus a 0.25% transfer fee (with a \\$1.50 minimum fee). On-chain payment credits the calling wallet's Laso account balance via the standard deposit webhook; the callable then debits the gross amount and queues the transfer.\n\n**A bank destination is required first.** `destination_id` comes from the banking callables: create the banking profile with `createBankingProfile`, register who is being paid with `createBankingRecipient`, and attach their bank account with `addBankingDestination`, which returns the id. List what you already have at `GET /bank-recipients` (free). See the [bank accounts guide](/guides/agent-bank-accounts) for the full setup.\n\n**Identity verification is required** on the account that owns the banking profile, and only the human owner can complete it. `createBankingProfile` hands back a `kycUrl` when it is outstanding.\n\n**If the payout cannot be fulfilled** (no approved banking profile, a destination that is not yours, an amount out of range), nothing is stranded: the USDC you paid has already credited your account balance. Fix the problem and retry, or recover it with `POST /withdraw`.\n\n**Settlement:** ACH, normally 1-2 business days. Follow it with `listBankingTransactions` / `getBankingTransaction`.\n\n**Fee:** 0.25% with a \\$1.50 minimum (included in the USDC price).",
        "x-x402": [
          {
            "price": "dynamic",
            "priceRange": "~$11.50–$50,125 USD (amount $10–$50,000 plus a 0.25% fee, min $1.50 fee)",
            "scheme": "exact",
            "network": "eip155:8453",
            "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
          },
          {
            "price": "dynamic",
            "priceRange": "~$11.50–$50,125 USD (amount $10–$50,000 plus a 0.25% fee, min $1.50 fee)",
            "scheme": "exact",
            "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
            "payTo": "3MZVk97x9SeRxbYpc3jhzRfU2fyA3emYutnqfn9kNfYX"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": null,
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Payout to a registered bank account",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "price": {
            "mode": "dynamic",
            "currency": "USD",
            "min": "11.50",
            "max": "50125"
          }
        },
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "USD delivered to the recipient's bank account (min \\$10, max \\$50,000). The x402 payment price is this amount plus a 0.25% fee (with a \\$1.50 minimum).",
            "schema": {
              "type": "number",
              "minimum": 10,
              "maximum": 50000
            }
          },
          {
            "name": "destination_id",
            "in": "query",
            "required": true,
            "description": "Bank destination to pay out to. Returned by `addBankingDestination`, and listed by `GET /bank-recipients`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bank payment queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string"
                    },
                    "user_id": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "bank_payment": {
                      "type": "object",
                      "properties": {
                        "payout_id": {
                          "type": "string"
                        },
                        "amount": {
                          "type": "number",
                          "description": "USD delivered to the bank account."
                        },
                        "fee_amount": {
                          "type": "number",
                          "description": "Transfer fee charged on top of the amount."
                        },
                        "charged_amount": {
                          "type": "number",
                          "description": "amount + fee_amount, the total debited."
                        },
                        "destination_id": {
                          "type": "string"
                        },
                        "state": {
                          "type": "string",
                          "example": "in-process"
                        },
                        "timestamp": {
                          "type": "number"
                        },
                        "destination": {
                          "type": "object",
                          "description": "The bank account being paid, summarized. The account number is masked to its last four digits.",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "bank_name": {
                              "type": "string"
                            },
                            "account_holder_name": {
                              "type": "string"
                            },
                            "account_number_last4": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or out-of-range `amount`, or a missing `destination_id`."
          },
          "402": {
            "description": "Payment required. Includes the x402 payment challenge. A **second** 402 on the paid retry means something different: the payment header verified but the transfer could not be settled on-chain. That response body is not empty and carries no `accepts` — it is the x402 settlement-failure shape `{\"success\": false, \"errorReason\": \"...\", \"errorMessage\": \"...\"}`, plus `x_laso_guidance` when a concrete next step applies. Distinguish the two by body: a challenge has `accepts`, a failure has `success: false`. The usual cause is an underfunded wallet, and the usual cause of that is the fee being charged **on top of** `amount` (a wallet holding exactly \\$2,000 cannot send a \\$2,000 payment). Nothing is charged for a failed settlement, so retrying with a smaller amount is safe."
          },
          "403": {
            "description": "Account is frozen."
          },
          "412": {
            "description": "No approved banking profile, or the destination is not one of yours. The USDC paid is credited to your account balance and recoverable with `POST /withdraw`."
          }
        }
      }
    },
    "/bank-recipients": {
      "get": {
        "operationId": "listBankRecipients",
        "x-mcp-duplicate-of": "listBankingRecipients",
        "summary": "List bank payout recipients (free)",
        "description": "Lists the recipients registered on your banking profile and the destinations attached to each, so you can find the `destination_id` that `GET /send-bank-payment` pays out to. Free — only the payout itself is a paid action.\n\nBank account numbers are returned masked to their last four digits; routing numbers are returned in full. Create recipients and destinations with the `createBankingRecipient` and `addBankingDestination` callables, rename a destination's `nickname` (the only editable field) with `updateBankingDestination`, and remove a recipient with `deleteBankingRecipient` (see the [bank accounts guide](/guides/agent-bank-accounts)).\n\nRequires a Bearer token from `/auth` or `/get-card`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The recipients on this banking profile.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "recipients": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "recipient_id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "address": {
                            "type": "object",
                            "description": "The payee's postal address, when one is set."
                          },
                          "destinations": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "destination_id": {
                                  "type": "string",
                                  "description": "Pass this as `destination_id` to `GET /send-bank-payment`."
                                },
                                "destination_type": {
                                  "type": "string",
                                  "enum": ["fiat_us", "fiat_iban", "crypto"]
                                },
                                "name": {
                                  "type": "string"
                                },
                                "nickname": {
                                  "type": "string",
                                  "description": "User-chosen label, present when one is set. The only editable field on a destination: set it at creation with `addBankingDestination` or later with `updateBankingDestination`."
                                },
                                "bank_name": {
                                  "type": "string"
                                },
                                "account_holder_name": {
                                  "type": "string"
                                },
                                "account_number_last4": {
                                  "type": "string"
                                },
                                "routing_number": {
                                  "type": "string"
                                },
                                "crypto_address": {
                                  "type": "string"
                                },
                                "network_id": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token."
          },
          "403": {
            "description": "Account is frozen."
          },
          "412": {
            "description": "No banking profile yet. Call `createBankingProfile` first."
          }
        }
      }
    },
    "/search-gift-cards": {
      "get": {
        "operationId": "searchGiftCards",
        "summary": "Search the gift card catalog",
        "description": "Browse and search available gift cards. Returns a list of gift card products with pricing, denomination, and catalog information. Use the `laso_server_id` from the results to order a card via `GET /order-gift-card`.\n\nEvery response includes a `facets` object listing the valid values for the `category`, `currency`, and `country` filters. The facets are computed from the full catalog, so a single unfiltered request (no query parameters) is enough to discover everything you can filter on.\n\nThis is a free endpoint — no x402 payment required. Requires a Bearer token from `/auth`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search query to filter gift cards by name (e.g. \"Amazon\", \"Uber\")",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "ISO 3166-1 alpha-2 country code to filter by (e.g. \"US\", \"GB\"). Returns gift cards available in that country, including borderless products that have no country restriction.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "description": "Currency code to filter by (e.g. \"USD\", \"EUR\")",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category to filter by (e.g. \"ecommerce\", \"travel\", \"gaming\"). See the `facets.categories` array in any response for the full list of valid values.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of matching gift card products",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "gift_cards": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GiftCardProduct"
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of results returned"
                    },
                    "filters": {
                      "type": "object",
                      "properties": {
                        "query": {
                          "type": ["string", "null"]
                        },
                        "country": {
                          "type": ["string", "null"]
                        },
                        "currency": {
                          "type": ["string", "null"]
                        },
                        "category": {
                          "type": ["string", "null"]
                        }
                      }
                    },
                    "facets": {
                      "type": "object",
                      "description": "Valid filter values, computed from the full catalog. Use these to discover what you can pass to the `category`, `currency`, and `country` query parameters.",
                      "properties": {
                        "categories": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "All valid `category` filter values."
                        },
                        "currencies": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "All valid `currency` filter values."
                        },
                        "countries": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "All valid `country` filter values (ISO 3166-1 alpha-2 codes)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing or invalid Authorization header"
                }
              }
            }
          }
        }
      }
    },
    "/order-gift-card": {
      "get": {
        "operationId": "orderGiftCard",
        "summary": "Order a gift card",
        "description": "Pay USDC via x402 to order a gift card. First browse the catalog via `GET /search-gift-cards` to find the `laso_server_id` for the card you want, then call this endpoint with the amount and product ID.\n\n**Pricing:** `amount` is the card's face value in the product's own currency, not USD. Laso converts it to USD at the current exchange rate and adds the product's fee (up to 4.8%); that total is the x402 USDC price. A 100 SAR card costs roughly \\$28 USDC, not \\$100. Check the product's `currency` field in `GET /search-gift-cards` to see what `amount` is denominated in.\n\nThe \\$5 minimum and \\$9,000 maximum apply to the **converted USD value**, not the raw amount, so a foreign-currency amount is accepted only when its USD equivalent falls inside that range.\n\nReturns redemption details (URL, code, and/or PIN) depending on the gift card brand.",
        "x-x402": [
          {
            "price": "dynamic",
            "priceRange": "$5–$9,000 USD (converted from the product's own currency)",
            "scheme": "exact",
            "network": "eip155:8453",
            "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
          },
          {
            "price": "dynamic",
            "priceRange": "$5–$9,000 USD (converted from the product's own currency)",
            "scheme": "exact",
            "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
            "payTo": "3MZVk97x9SeRxbYpc3jhzRfU2fyA3emYutnqfn9kNfYX"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "amount": null,
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Gift card, charged at the product's USD cost",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "price": {
            "mode": "dynamic",
            "currency": "USD",
            "min": "5",
            "max": "9432"
          }
        },
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "Gift card face value in the product's own currency (see the product's `currency` field in `GET /search-gift-cards`), not USD. The x402 price is this value converted to USD plus the product fee. After conversion it must be worth at least \\$5 and at most \\$9,000 USD.",
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0
            }
          },
          {
            "name": "laso_server_id",
            "in": "query",
            "required": true,
            "description": "The product identifier from the gift card catalog (GET /search-gift-cards). Always copy it from a search result; an id that is not in the catalog returns 404 `unknown_laso_server_id`. Amazon is `amazon` for every country, selected with `country`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "ISO 3166-1 alpha-2 country code (defaults to \"US\")",
            "schema": {
              "type": "string",
              "default": "US"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Gift card ordered successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "callable_base_url": {
                      "type": "string",
                      "example": "https://laso.finance"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The user's ID (lowercase wallet address)"
                    },
                    "gift_card": {
                      "$ref": "#/components/schemas/GiftCardOrder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "laso_server_id query parameter is required"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. No valid x402 payment header was included. The response body is an empty JSON object; the payment details (price, recipient address, network) are base64-encoded in the `PAYMENT-REQUIRED` response header. x402 client libraries handle this automatically. A **second** 402 on the paid retry means something different: the payment header verified but the transfer could not be settled on-chain. That response body is not empty and carries no `accepts` — it is the x402 settlement-failure shape `{\"success\": false, \"errorReason\": \"...\", \"errorMessage\": \"...\"}`, plus `x_laso_guidance` when a concrete next step applies. Distinguish the two by body: a challenge has `accepts`, a failure has `success: false`. The usual cause is an underfunded wallet, and the usual cause of that is the fee being charged **on top of** `amount` (a wallet holding exactly \\$2,000 cannot send a \\$2,000 payment). Nothing is charged for a failed settlement, so retrying with a smaller amount is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {}
                }
              }
            }
          },
          "403": {
            "description": "Account is frozen. The response includes a `frozen_message` field explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrozenError"
                },
                "example": {
                  "error": "Account is frozen",
                  "frozen_message": "Your account is frozen pending a compliance review. Contact support@laso.finance."
                }
              }
            }
          },
          "404": {
            "description": "`laso_server_id` matches no gift card in the catalog (`code: unknown_laso_server_id`). The check runs before a payment is quoted, so this is returned in place of the 402 challenge and nothing is charged. It is terminal: the same id will never succeed. Look up a valid id with `GET /search-gift-cards` and order with that value.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": ["unknown_laso_server_id"]
                    },
                    "terminal": {
                      "type": "boolean"
                    },
                    "laso_server_id": {
                      "type": "string",
                      "description": "The id that was sent"
                    },
                    "hint": {
                      "type": "string"
                    },
                    "search_url": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "error": "No gift card in the catalog has laso_server_id \"amazon-usa\".",
                  "code": "unknown_laso_server_id",
                  "terminal": true,
                  "laso_server_id": "amazon-usa",
                  "hint": "Find a valid laso_server_id with GET /search-gift-cards (for example /search-gift-cards?q=amazon) and order with that value. No payment was taken for this request.",
                  "search_url": "https://laso.finance/search-gift-cards"
                }
              }
            }
          }
        }
      }
    },
    "/refresh": {
      "post": {
        "operationId": "refreshTokenDeprecated",
        "summary": "Refresh an ID token (deprecated)",
        "description": "**Deprecated:** Use `POST /auth` with `grant_type=refresh_token` instead. This endpoint is kept for backward compatibility.\n\nExchange a refresh token for a new ID token. This is a free endpoint — no x402 payment required.",
        "deprecated": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["refresh_token"],
                "properties": {
                  "refresh_token": {
                    "type": "string",
                    "description": "The refresh token received from /auth or a previous refresh call"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refreshed auth credentials and user ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auth": {
                      "$ref": "#/components/schemas/AuthCredentials"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The user's ID (lowercase wallet address)"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing refresh_token in request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "refresh_token is required in request body"
                }
              }
            }
          },
          "401": {
            "description": "Token is invalid or revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Failed to refresh token. Token may be invalid or revoked."
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/createBankingProfile": {
      "post": {
        "operationId": "createBankingProfile",
        "summary": "Create the banking profile",
        "tags": ["Banking"],
        "description": "Opens the banking profile that all other banking calls hang off. Identity verification is required first; if the human has not verified, the reply carries `kycRequired: true` and a `kycUrl` to hand them. **This is the only step an agent cannot complete itself.** Retry once they finish.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The profile, or a `kycRequired` reply with the link to give the human.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/getBankingProfileStatus": {
      "post": {
        "operationId": "getBankingProfileStatus",
        "summary": "Check the banking profile status",
        "tags": ["Banking"],
        "description": "Reads the current state of the banking profile: whether it exists, and where its application stands.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The profile status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/getBankingApplication": {
      "post": {
        "operationId": "getBankingApplication",
        "summary": "Read the banking application",
        "tags": ["Banking"],
        "description": "Reads what the application still needs. Returns `ready` (whether it can be submitted), `applicationStatus`, and the outstanding questions. The identity answers carry over from verification; what remains are non-identity questions such as employment status and source of funds, which an agent can answer itself.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The application, its status, and what is outstanding.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/updateBankingApplicationDetails": {
      "post": {
        "operationId": "updateBankingApplicationDetails",
        "summary": "Answer application questions",
        "tags": ["Banking"],
        "description": "Submits answers to the non-identity application questions read from `getBankingApplication`. Call as many times as needed; each call merges.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "details"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "details": {
                        "type": "object",
                        "description": "The answers to submit, keyed by the field names `getBankingApplication` reported as outstanding."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated application.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/uploadBankingDocument": {
      "post": {
        "operationId": "uploadBankingDocument",
        "summary": "Upload a proof-of-address document",
        "tags": ["Banking"],
        "description": "Attaches a proof-of-address document to the banking application, for the cases where the partner asks for one. The application must already exist and have an individual on it, so complete the application first.\n\n**Two things reject uploads that otherwise look fine.** A `.jpg` file must be sent as `fileType: \"jpeg\"` — `\"jpg\"` is refused. And the partner caps the *decoded* size at 10MB, even though their own upload page advertises 20MB; oversized files are rejected here before the round-trip.\n\n`fileContent` accepts either a bare base64 string or a `data:` URL, so a browser `FileReader.readAsDataURL` result can be passed through unchanged.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": [
                      "userId",
                      "documentType",
                      "fileType",
                      "fileContent",
                      "country"
                    ],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "documentType": {
                        "type": "string",
                        "enum": [
                          "proof_of_address",
                          "bank_statement",
                          "utility_bill"
                        ],
                        "description": "The kind of document.",
                        "example": "utility_bill"
                      },
                      "fileType": {
                        "type": "string",
                        "enum": ["pdf", "jpeg", "png"],
                        "description": "The file's format. Send a .jpg as `jpeg`.",
                        "example": "jpeg"
                      },
                      "fileContent": {
                        "type": "string",
                        "description": "The file, base64-encoded. A `data:` URL is also accepted. Decoded size must be 10MB or less."
                      },
                      "country": {
                        "type": "string",
                        "description": "The document's country, ISO 3166-1 alpha-2.",
                        "example": "US"
                      },
                      "filename": {
                        "type": "string",
                        "description": "The file's name.",
                        "example": "utility-bill.pdf"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created document id and its review status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object",
                      "properties": {
                        "documentId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          },
          "412": {
            "description": "The application has no individual to attach a document to yet. Complete the application first."
          }
        }
      }
    },
    "/submitBankingApplication": {
      "post": {
        "operationId": "submitBankingApplication",
        "summary": "Submit the application",
        "tags": ["Banking"],
        "description": "Submits the completed application for review. If the partner rejects it as incomplete, the reply says what is still missing rather than failing opaquely.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether the submission was accepted, and what is missing if not.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/createBankingAccount": {
      "post": {
        "operationId": "createBankingAccount",
        "summary": "Open an on-ramp or off-ramp account",
        "tags": ["Banking"],
        "description": "Opens a banking rail. An **on-ramp** issues a virtual US bank account: dollars sent to it arrive as USDC in the managed wallet. An **off-ramp** pays out to a bank account you have registered, and is opened automatically by `/send-bank-payment`, so most agents never call this for one directly.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "accountType"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "accountType": {
                        "type": "string",
                        "enum": ["onramp", "offramp"],
                        "description": "Which rail to open."
                      },
                      "cryptoAddress": {
                        "type": "string",
                        "description": "On-ramp only. Where the converted USDC should land. Defaults to the managed agent wallet."
                      },
                      "networkId": {
                        "type": "string",
                        "description": "On-ramp only. Defaults to Solana mainnet."
                      },
                      "asset": {
                        "type": "string",
                        "description": "Crypto asset ticker, uppercase.",
                        "example": "USDC"
                      },
                      "fiatDestinationId": {
                        "type": "string",
                        "description": "Off-ramp only. The bank destination payouts go to, from `addBankingDestination`."
                      },
                      "capabilities": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "On-ramp only. Bank rails to support. Defaults to ACH and wire."
                      },
                      "rail": {
                        "type": "string",
                        "description": "The payment rail for the account."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The account, including its bank details for an on-ramp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/listBankingAccounts": {
      "post": {
        "operationId": "listBankingAccounts",
        "summary": "List banking accounts",
        "tags": ["Banking"],
        "description": "Lists the account's banking rails and their details — for an on-ramp, the routing and account numbers to give whoever is paying in. The off-ramp deposit address is deliberately withheld: it is internal payout plumbing, not account detail.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/createBankingRecipient": {
      "post": {
        "operationId": "createBankingRecipient",
        "summary": "Create a payout recipient",
        "tags": ["Banking"],
        "description": "Creates a person or company money can be sent to. Bank details are attached separately with `addBankingDestination`.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "name"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "name": {
                        "type": "string",
                        "description": "The recipient's name.",
                        "example": "Acme LLC"
                      },
                      "address": {
                        "type": "object",
                        "description": "The recipient's postal address. Required before a bank destination can be attached, so supplying it here avoids a second call."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recipient.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/addBankingDestination": {
      "post": {
        "operationId": "addBankingDestination",
        "summary": "Attach bank details to a recipient",
        "tags": ["Banking"],
        "description": "Attaches a bank account or crypto address to a recipient and returns the `destination_id` that `/send-bank-payment` pays out to.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "recipientId", "destination"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "recipientId": {
                        "type": "string",
                        "description": "The recipient from `createBankingRecipient`."
                      },
                      "destination": {
                        "type": "object",
                        "description": "The destination. For a US bank account: `destination_type: \"fiat_us\"` plus `aba_routing_number`, `account_number`, `account_type`, `account_holder_name`, and `bank_name`. Also supports `fiat_iban` and `crypto`."
                      },
                      "nickname": {
                        "type": "string",
                        "description": "An optional label. The only field editable later, via `updateBankingDestination`.",
                        "example": "Rent account"
                      },
                      "recipientAddress": {
                        "type": "object",
                        "description": "Sets the recipient's postal address before attaching the destination. Needed when the recipient was created name-only: bank destinations are rejected without one, so supplying it here avoids a dead end."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The destination, including its `destination_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/listBankingRecipients": {
      "post": {
        "operationId": "listBankingRecipients",
        "summary": "List recipients and destinations",
        "tags": ["Banking"],
        "description": "Lists payout recipients and their destinations, with account numbers masked to the last four digits. This is where `destination_id` values come from. Also available as the free paywalled route `GET /bank-recipients`.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recipients and their destinations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/updateBankingDestination": {
      "post": {
        "operationId": "updateBankingDestination",
        "summary": "Rename a destination",
        "tags": ["Banking"],
        "description": "Sets or clears a destination's nickname. This is the only editable field: bank details are immutable at the partner, so anything else wrong means deleting the recipient and creating a new one.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "destinationId", "nickname"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "destinationId": {
                        "type": "string",
                        "description": "The destination to rename."
                      },
                      "nickname": {
                        "type": "string",
                        "description": "The new nickname. An empty string clears it.",
                        "example": "Rent account"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/deleteBankingRecipient": {
      "post": {
        "operationId": "deleteBankingRecipient",
        "summary": "Delete a recipient",
        "tags": ["Banking"],
        "description": "Deletes a recipient, its destinations, and any off-ramp account that paid out to it. Irreversible.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "recipientId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "recipientId": {
                        "type": "string",
                        "description": "The recipient to delete."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/getBankingTransaction": {
      "post": {
        "operationId": "getBankingTransaction",
        "summary": "Read one transaction",
        "tags": ["Banking"],
        "description": "Reads a single banking transaction — an on-ramp arriving or an off-ramp paying out — with its normalized status.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "transactionId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "transactionId": {
                        "type": "string",
                        "description": "The transaction to read."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The transaction.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/listBankingTransactions": {
      "post": {
        "operationId": "listBankingTransactions",
        "summary": "List transactions",
        "tags": ["Banking"],
        "description": "Lists banking transactions for the account: money arriving over the on-ramp and paying out over the off-ramp, with normalized statuses and amounts.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The transactions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "The caller is not this user and is not an admin."
          }
        }
      }
    },
    "/agentAuth": {
      "post": {
        "operationId": "exchangeApiKey",
        "summary": "Exchange an API key for a session",
        "tags": ["Agent Wallet"],
        "description": "Exchanges a durable `lasoak_` API key for a short-lived `id_token` (one hour) plus a `refresh_token`, and returns an `auth_url` to hand your human so they can watch what you do. This is the first call an agent makes. The reply's `id_token` authenticates every other endpoint here.\n\nIf you have no key, your human creates one at https://laso.finance/agent/dashboard — there is no other way to obtain one.\n\nSend the `id_token` from `/auth` as a Bearer token, with a plain JSON body (no `data` wrapper). Free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["api_key"],
                "properties": {
                  "api_key": {
                    "type": "string",
                    "description": "Your `lasoak_` API key.",
                    "example": "lasoak_..."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An id_token, a refresh_token, and an auth_url for your human.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          }
        }
      }
    },
    "/announceAgentConnection": {
      "post": {
        "operationId": "announceAgentConnection",
        "summary": "Announce that you connected",
        "tags": ["Agent Wallet"],
        "description": "**Required immediately after sign-in, before anything else.** Your human is on a \"waiting for your agent\" screen that only flips to \"connected\" when this lands; until then a successful sign-in is indistinguishable from a broken one on their side. Skipping it is the most common agent mistake here, and the one humans actually notice.\n\nThe reply also carries the wallet's funding state, so it doubles as a first `getAgentWallet` call.\n\nSend the `id_token` from `/auth` as a Bearer token, with a plain JSON body (no `data` wrapper). Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agentName": {
                    "type": "string",
                    "description": "What to call you on their dashboard.",
                    "example": "Claude"
                  },
                  "message": {
                    "type": "string",
                    "description": "A short free-form note shown alongside the connection."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation, plus the wallet's current funding state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/getAgentWallet": {
      "get": {
        "operationId": "getAgentWallet",
        "summary": "Read the managed wallet",
        "tags": ["Agent Wallet"],
        "description": "Returns the managed wallet's Solana address and USDC balance: `wallet_address`, `balance_usdc`, `needs_funding`, and `has_wallet`.\n\n`wallet_address` is the only place to send funds, and it is **not** your `user_id` — a `usr_...` value is an account id, never a receive address. Poll this to watch a deposit land.\n\nSend the `id_token` from `/auth` as a Bearer token, with a plain JSON body (no `data` wrapper). Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The wallet address, balance, and funding state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/createAgentWallet": {
      "post": {
        "operationId": "createAgentWallet",
        "summary": "Provision a managed wallet",
        "tags": ["Agent Wallet"],
        "description": "Creates the Laso-custodied Solana spend wallet for the account, or returns the existing one. Idempotent. Most agents never call this: the wallet is provisioned when the human completes setup on the dashboard.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    },
                    "required": ["userId"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The wallet id and its Solana address.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/agentX402Pay": {
      "post": {
        "operationId": "agentX402Pay",
        "summary": "Pay an x402 endpoint from the managed wallet",
        "tags": ["Agent Wallet"],
        "description": "Settles an x402 payment on your behalf, so you never build a payment header or hold a private key. Takes either a Laso `route` name or the full `url` of any external x402 endpoint on the open web.\n\nFor an external `url`, pin `maxAmountUsdc` plus optionally `expectedAsset` / `expectedNetwork` / `expectedPayTo`; a challenge that exceeds or does not match them is refused.\n\nThe call succeeds as long as the request went out, so check the returned `status` — it is the endpoint's status, not the callable's.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "route": {
                        "type": "string",
                        "description": "A Laso route name, e.g. `send-bank-payment`. Mutually exclusive with `url`."
                      },
                      "url": {
                        "type": "string",
                        "description": "Full URL of an external x402 endpoint. Mutually exclusive with `route`."
                      },
                      "params": {
                        "type": "object",
                        "description": "Query parameters for the route."
                      },
                      "method": {
                        "type": "string",
                        "description": "GET or POST. Defaults to GET."
                      },
                      "maxAmountUsdc": {
                        "type": "number",
                        "description": "Required for an external `url`: the most you will pay, in whole USDC. Pin the FEE-INCLUSIVE total, not the amount you requested: most priced routes add a fee on top of `amount` and round it up to the cent, so the 402 challenge costs more than the number you passed in. Take the price from the challenge's `amount` field (atomic units; divide by 1000000 for USDC) rather than recomputing it. A cap derived from the pre-fee amount lands under the real price and refuses your own payment. It can only tighten the account's per-payment spend limit, never raise it.",
                        "example": 5
                      },
                      "expectedAsset": {
                        "type": "string",
                        "description": "Refuse the challenge if the asset differs."
                      },
                      "expectedNetwork": {
                        "type": "string",
                        "description": "Refuse the challenge if the network differs."
                      },
                      "expectedPayTo": {
                        "type": "string",
                        "description": "Refuse the challenge if the payee differs."
                      },
                      "body": {
                        "type": "object",
                        "description": "JSON body for the paid request, when `method` is POST."
                      }
                    },
                    "required": ["userId"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The paid call's `status`, `body`, and an `error` when the endpoint refused.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/agentWalletTransfer": {
      "post": {
        "operationId": "agentWalletTransfer",
        "summary": "Send USDC from the managed wallet",
        "tags": ["Agent Wallet"],
        "description": "Moves USDC out of the managed wallet to any Solana address, or to any USDC address on Base. Laso signs on your behalf.\n\nTo send to Base, set `destinationNetwork` to `base` and pass a `0x` address. The managed wallet is a Solana wallet, so Laso bridges the USDC over CCTP and it lands as native USDC on Base, usually within a minute. `txHash` is the Solana transaction out of the managed wallet, not the Base delivery. The minimum is \\$1 and the maximum is the account's per-payment spend limit (see `getAgentSpendLimit`). The recipient receives the amount less a 0.01% bridge fee. If the bridge fails, nothing is burned and the amount is credited to your Laso account balance instead.\n\nTakes an address, and deliberately **not** an address-book name: a rename between your human asking and you sending would move money somewhere they did not intend. When you resolve a name via `listAddressBook`, say the address back to them before sending.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId", "destinationAddress", "amount"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "destinationAddress": {
                        "type": "string",
                        "description": "The recipient's address: a Solana address, or a `0x` address when `destinationNetwork` is `base`.",
                        "example": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
                      },
                      "destinationNetwork": {
                        "type": "string",
                        "enum": ["solana", "base"],
                        "default": "solana",
                        "description": "The chain `destinationAddress` is on. `base` is delivered by bridging over CCTP."
                      },
                      "amount": {
                        "type": "string",
                        "description": "Decimal USDC string, not lamports.",
                        "example": "5.50"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The `transferId`, `txHash`, `destinationAddress`, and `destinationNetwork`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    },
    "/getAgentSpendLimit": {
      "post": {
        "operationId": "getAgentSpendLimit",
        "summary": "Read the per-payment spend limit",
        "tags": ["Agent Wallet"],
        "description": "Returns the maximum the account owner allows for any single payment from the managed wallet: `maxPerPaymentUsdc`, plus the `min`, `max`, and `default` bounds it can be set within, all in whole USDC. The limit defaults to \\$1,000 and ranges from \\$1 to \\$50,000. It applies to every payment `agentX402Pay` makes, Laso routes and external `url` endpoints alike.\n\nCheck an amount against this **before** attempting a payment rather than waiting to be refused. Compare the fee-inclusive total, since fees are added on top of the amount you request. Read-only to an agent: only the account owner can change the limit, from the dashboard. If a payment would exceed it, do not split it into smaller payments. Stop and tell your human.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "required": ["userId"],
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth). Must be your own.",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The current limit and its bounds, in whole USDC.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object",
                      "properties": {
                        "maxPerPaymentUsdc": {
                          "type": "number",
                          "description": "The most a single payment may total, fees included.",
                          "example": 1000
                        },
                        "min": {
                          "type": "number",
                          "description": "The lowest value the owner can set.",
                          "example": 1
                        },
                        "max": {
                          "type": "number",
                          "description": "The highest value the owner can set.",
                          "example": 50000
                        },
                        "default": {
                          "type": "number",
                          "description": "The limit that applies when the owner has never set one.",
                          "example": 1000
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          },
          "403": {
            "description": "`userId` is not the caller's own account."
          }
        }
      }
    },
    "/createAgentApiKey": {
      "post": {
        "operationId": "createAgentApiKey",
        "summary": "Issue an API key",
        "tags": ["Agent Wallet"],
        "description": "Issues a durable `lasoak_` API key for the account. **The raw key is returned once** — only its hash is stored, so it cannot be retrieved again. Normally the human does this from the dashboard.\n\nA Firebase callable. Callables wrap the request in `{\"data\": ...}` and the reply in `{\"result\": ...}`. Send the `id_token` from `/auth` as a Bearer token. Free.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["data"],
                "properties": {
                  "data": {
                    "type": "object",
                    "properties": {
                      "userId": {
                        "type": "string",
                        "description": "The Laso user id (the `usr_...` value from /auth).",
                        "example": "usr_7aff81763cae58c936aa6de67dd5a95e"
                      },
                      "label": {
                        "type": "string",
                        "description": "A label to recognize the key by.",
                        "example": "Claude Code"
                      }
                    },
                    "required": ["userId"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The raw `apiKey` (shown once) and its `keyId`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid."
          },
          "401": {
            "description": "Missing, invalid, or expired Bearer token."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AuthCredentials": {
        "type": "object",
        "properties": {
          "id_token": {
            "type": "string",
            "description": "ID token — use as Bearer token for Laso Finance APIs"
          },
          "refresh_token": {
            "type": "string",
            "description": "Use with POST /auth (grant_type=refresh_token) to get a new id_token when it expires"
          },
          "expires_in": {
            "type": "string",
            "description": "Token lifetime in seconds"
          }
        }
      },
      "CardOrder": {
        "type": "object",
        "description": "Card order info returned by /get-card. Status is always `pending` initially — poll `/get-card-data` to check when card details are ready.",
        "properties": {
          "card_id": {
            "type": "string"
          },
          "usd_amount": {
            "type": "number"
          },
          "country": {
            "type": "string",
            "example": "US"
          },
          "timestamp": {
            "type": "number"
          },
          "timestamp_readable": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["pending"],
            "description": "Always `pending` at order time. Poll `/get-card-data` to check for `ready`."
          }
        }
      },
      "CardData": {
        "type": "object",
        "description": "Response from `/get-card-data`. Three card types share this shape and each populates a different subset.\n\n**Non-reloadable (U.S. and International):** when `status` is `ready`, `card_details` carries the number, CVV, and expiry. International cards add `label`, `charged_usd_amount`, `fees_paid`, `state`, `balance_update_requested_timestamp`, and `queued_order_card_id`.\n\n**Reloadable:** returns `last4`, `expiry`, `balance`, `spend_limit`, `reusable`, `issuer_status`, `created_at`, and `expires_at` instead of the `usd_amount`/`timestamp` fields above. Reading its number and CVV is gated by the card issuer, so exactly one of `card_details`, `details_approval`, or `details_error` is present on a single-card lookup.",
        "properties": {
          "card_id": {
            "type": "string"
          },
          "card_type": {
            "type": "string",
            "enum": [
              "Non-Reloadable U.S.",
              "Non-Reloadable International",
              "Reloadable"
            ]
          },
          "usd_amount": {
            "type": "number"
          },
          "country": {
            "type": "string",
            "description": "U.S. cards only.",
            "example": "US"
          },
          "label": {
            "type": "string",
            "description": "International cards only. User-supplied label, may be empty.",
            "nullable": true
          },
          "charged_usd_amount": {
            "type": "number",
            "description": "International cards only. Amount the user was charged including fees."
          },
          "fees_paid": {
            "type": "number",
            "description": "International cards only. Fees paid for this card."
          },
          "state": {
            "type": "string",
            "description": "International cards only. Raw card state.",
            "enum": [
              "queued",
              "redeemable",
              "complete",
              "refund-requested",
              "refund-requested-approved-for-queue",
              "archived",
              "refunded"
            ]
          },
          "balance_update_requested_timestamp": {
            "type": "number",
            "nullable": true,
            "description": "International cards only. Unix timestamp (ms) of an outstanding admin balance update request, or null if none is pending."
          },
          "queued_order_card_id": {
            "type": "string",
            "nullable": true,
            "description": "International cards only. The original `card_id` returned by `/order-intl-card`. After admin fulfillment the card is reissued with a new `card_id` (the issuer's transaction id); querying `/get-card-data?card_id=<original>` continues to resolve to the fulfilled card via this field."
          },
          "timestamp": {
            "type": "number"
          },
          "timestamp_readable": {
            "type": "string"
          },
          "last_updated_timestamp": {
            "type": "number",
            "description": "U.S. cards only. Unix timestamp (ms) of the last time card data was refreshed."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "queued",
              "complete",
              "refund-requested",
              "refunded",
              "archived"
            ]
          },
          "card_details": {
            "type": "object",
            "description": "Only present when status is `ready` (U.S.) or `complete` (international).",
            "properties": {
              "card_number": {
                "type": "string"
              },
              "exp_month": {
                "type": "string"
              },
              "exp_year": {
                "type": "string"
              },
              "cvv": {
                "type": "string"
              },
              "available_balance": {
                "type": "number"
              },
              "billing_address": {
                "type": "object",
                "description": "The card billing address to enter when a merchant asks for one at checkout. The billing name is always `Laso Finance`. For USA non-reloadable cards `required` is `false` (any valid U.S. billing address works; this is a known-good default). For international non-reloadable cards `required` is `true` and the merchant AVS check is validated against exactly this address. On reloadable cards this is always null: the card issuer holds no billing address for a card, and they are AVS-checked against the address the account holder gave at their own identity verification. Ask the holder for it; do not substitute Laso's address.",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Always `Laso Finance`."
                  },
                  "line_1": {
                    "type": "string"
                  },
                  "line_2": {
                    "type": "string"
                  },
                  "city": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string"
                  },
                  "zip": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string"
                  },
                  "required": {
                    "type": "boolean",
                    "description": "`true` if the card enforces AVS against this exact address (international cards); `false` if any valid U.S. address works (USA cards)."
                  },
                  "note": {
                    "type": "string"
                  }
                },
                "nullable": true
              }
            }
          },
          "transactions": {
            "type": "array",
            "description": "Card transaction history. U.S. and international cards use slightly different shapes — see `CardTransaction` and `IntlCardTransaction`.",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/CardTransaction"
                },
                {
                  "$ref": "#/components/schemas/IntlCardTransaction"
                }
              ]
            }
          },
          "last4": {
            "type": "string",
            "description": "Reloadable cards only. Last four digits.",
            "example": "8260"
          },
          "expiry": {
            "type": "string",
            "description": "Reloadable cards only. MM/YY.",
            "example": "07/31"
          },
          "balance": {
            "type": "number",
            "description": "Reloadable cards only. Spendable balance in dollars.",
            "example": 208
          },
          "spend_limit": {
            "type": "number",
            "description": "Reloadable cards only. Spend cap in dollars.",
            "example": 208
          },
          "reusable": {
            "type": "boolean",
            "description": "Reloadable cards only. True for a multi-use card."
          },
          "issuer_status": {
            "type": "string",
            "description": "Reloadable cards only. The issuer's own status string, kept verbatim.",
            "example": "OPEN"
          },
          "created_at": {
            "type": "number",
            "description": "Reloadable cards only. Epoch milliseconds."
          },
          "expires_at": {
            "type": "number",
            "nullable": true,
            "description": "Reloadable cards only. Epoch milliseconds, or null when the card does not expire."
          },
          "details_approval": {
            "type": "object",
            "nullable": true,
            "description": "Reloadable cards only, and only for a card the holder created in ANOTHER app. The issuer has emailed them an approve/deny link; retry with `approval_id` once they approve. Cards created through `/create-reloadable-card` are Laso-issued and never take this path.",
            "properties": {
              "status": {
                "type": "string",
                "enum": ["pending"]
              },
              "approval_id": {
                "type": "string",
                "nullable": true
              },
              "note": {
                "type": "string"
              }
            }
          },
          "details_error": {
            "type": "string",
            "nullable": true,
            "description": "Reloadable cards only. Present when the issuer could not return the number, so a card with no `card_details` is never silently indistinguishable from one whose details are pending."
          }
        }
      },
      "CardTransaction": {
        "type": "object",
        "description": "U.S. prepaid card transaction.",
        "properties": {
          "amount": {
            "type": "number"
          },
          "date": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "is_credit": {
            "type": "boolean"
          }
        }
      },
      "IntlCardTransaction": {
        "type": "object",
        "description": "International prepaid card transaction.",
        "properties": {
          "amount": {
            "type": "number",
            "description": "Transaction amount in USD."
          },
          "date": {
            "type": "number",
            "nullable": true,
            "description": "Unix timestamp (ms) of the transaction."
          },
          "merchant": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "nullable": true,
            "description": "Optional status, e.g. `pending`."
          }
        }
      },
      "AccountBalance": {
        "type": "object",
        "description": "Account balance information.",
        "properties": {
          "user_id": {
            "type": "string",
            "description": "The user's ID (lowercase wallet address)"
          },
          "balance": {
            "type": "number",
            "description": "Current available balance in USD"
          },
          "total_deposits": {
            "type": "number",
            "description": "Total lifetime deposits in USD"
          },
          "created_timestamp": {
            "type": "number"
          },
          "created_timestamp_readable": {
            "type": "string"
          }
        }
      },
      "WithdrawalRecord": {
        "type": "object",
        "description": "A withdrawal record.",
        "properties": {
          "id": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          },
          "state": {
            "type": "string",
            "enum": ["pending"]
          },
          "solana_address": {
            "type": "string",
            "description": "The Solana address the USDC will be sent to."
          },
          "timestamp": {
            "type": "number"
          },
          "timestamp_readable": {
            "type": "string"
          }
        }
      },
      "WithdrawalStatus": {
        "type": "object",
        "description": "A withdrawal status record.",
        "properties": {
          "id": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          },
          "asset": {
            "type": "string",
            "example": "USDC"
          },
          "network": {
            "type": "string",
            "example": "BASE_MAINNET"
          },
          "state": {
            "type": "string"
          },
          "address": {
            "type": "string",
            "description": "The wallet address the withdrawal is sent to"
          },
          "timestamp": {
            "type": "number"
          },
          "timestamp_readable": {
            "type": "string"
          },
          "tx_hash": {
            "type": "string",
            "description": "On-chain transaction hash, present once the withdrawal is processed"
          },
          "tx_url": {
            "type": "string",
            "description": "Block explorer URL for the transaction, present when tx_hash exists"
          }
        }
      },
      "PaymentStatus": {
        "type": "object",
        "description": "One Venmo or PayPal payout sent from this account.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass as `payment_id` to fetch this payout on its own."
          },
          "amount_pre_fees": {
            "type": "number",
            "description": "Dollars the recipient receives."
          },
          "amount_with_fees": {
            "type": "number",
            "description": "Dollars debited from the account balance, fee included."
          },
          "fees_paid": {
            "type": "number"
          },
          "platform": {
            "type": "string",
            "enum": ["venmo", "paypal"]
          },
          "state": {
            "type": "string",
            "enum": ["queued", "in-process", "complete", "failed", "cancelled"],
            "description": "`queued` and `in-process` are still in flight. Absent on payouts sent before state tracking existed."
          },
          "state_updated_at": {
            "type": "integer",
            "description": "Milliseconds since the epoch when `state` last changed."
          },
          "recipient_id": {
            "type": "string",
            "description": "The saved recipient this went to, as returned by `GET /payment-recipients`."
          },
          "recipient_first_name": {
            "type": "string"
          },
          "recipient_last_name": {
            "type": "string"
          },
          "timestamp": {
            "type": "integer",
            "description": "Milliseconds since the epoch when the payout was sent."
          },
          "timestamp_readable": {
            "type": "string"
          }
        }
      },
      "MerchantResult": {
        "type": "object",
        "description": "A merchant from the spend data database with its acceptance status for the Non-Reloadable U.S. card.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Merchant name",
            "example": "Amazon"
          },
          "url": {
            "type": "string",
            "description": "Merchant website URL",
            "example": "amazon.com"
          },
          "status": {
            "type": "string",
            "enum": ["accepted", "not_accepted", "unknown"],
            "description": "Whether the Non-Reloadable U.S. card is accepted at this merchant. `accepted` = confirmed working, `not_accepted` = confirmed failing or restricted, `unknown` = card type not yet tried at this merchant."
          },
          "restriction_note": {
            "type": ["string", "null"],
            "description": "Present when the merchant is restricted rather than merely untested. Banking and money-transfer merchants are always reported as `not_accepted`, because spending there counts as a transfer of value. Approvals do occur at these merchants, so acceptance cannot be read off the transaction history. `null` for every other merchant."
          },
          "description": {
            "type": ["string", "null"],
            "description": "Brief description of the merchant"
          },
          "notes": {
            "type": ["string", "null"],
            "description": "Additional notes about using the card at this merchant"
          }
        }
      },
      "GiftCardProduct": {
        "type": "object",
        "description": "A gift card product from the catalog.",
        "properties": {
          "laso_server_id": {
            "type": "string",
            "description": "Product identifier to use when ordering via GET /order-gift-card"
          },
          "name": {
            "type": "string",
            "description": "Gift card brand name"
          },
          "description": {
            "type": "string"
          },
          "category": {
            "type": ["string", "null"],
            "description": "Catalog category for the gift card (e.g. \"ecommerce\", \"travel\")."
          },
          "country": {
            "type": ["string", "null"],
            "example": "FR",
            "description": "ISO 3166-1 alpha-2 country the gift card belongs to. Null for borderless products available in more than one country."
          },
          "currency": {
            "type": ["string", "null"],
            "example": "USD",
            "description": "Currency code for the gift card"
          },
          "min": {
            "type": "number",
            "description": "Minimum order amount"
          },
          "max": {
            "type": "number",
            "description": "Maximum order amount"
          },
          "increment": {
            "type": "string",
            "description": "Minimum order increment"
          },
          "denominations": {
            "type": ["array", "null"],
            "items": {
              "type": "string"
            },
            "description": "If set, only these exact amounts can be ordered. Null for range-based products."
          },
          "product_image_url": {
            "type": "string",
            "description": "URL of the gift card image"
          },
          "catalog_info": {
            "type": ["object", "null"],
            "properties": {
              "brand_description": {
                "type": ["string", "null"]
              },
              "redemption_instructions": {
                "type": ["string", "null"]
              }
            }
          }
        }
      },
      "GiftCardOrder": {
        "type": "object",
        "description": "Gift card order result with redemption details.",
        "properties": {
          "card_id": {
            "type": "string"
          },
          "laso_server_id": {
            "type": "string"
          },
          "amount": {
            "type": "number",
            "description": "Gift card face value, denominated in the product's own `currency` (not USD)"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code the `amount` is denominated in, e.g. `USD`, `SAR`, `EUR`",
            "example": "USD"
          },
          "country": {
            "type": "string",
            "example": "US"
          },
          "redemption_url": {
            "type": ["string", "null"],
            "description": "URL to redeem the gift card (if applicable)"
          },
          "redemption_code": {
            "type": ["string", "null"],
            "description": "Code to redeem the gift card (if applicable)"
          },
          "pin_code": {
            "type": ["string", "null"],
            "description": "PIN code for the gift card (if applicable)"
          },
          "status": {
            "type": "string",
            "enum": ["completed"]
          },
          "timestamp": {
            "type": "number"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "FrozenError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "Account is frozen"
          },
          "frozen_message": {
            "type": "string",
            "description": "Human-readable explanation of why the account is frozen"
          }
        }
      }
    },
    "headers": {
      "RateLimit-Limit": {
        "description": "Requests permitted in the current window.",
        "schema": {
          "type": "integer"
        },
        "example": 120
      },
      "RateLimit-Remaining": {
        "description": "Requests still available in the current window.",
        "schema": {
          "type": "integer"
        },
        "example": 118
      },
      "RateLimit-Reset": {
        "description": "Seconds until the window rolls over.",
        "schema": {
          "type": "integer"
        },
        "example": 42
      },
      "RateLimit-Policy": {
        "description": "The policy the other headers describe, as `limit;w=seconds`.",
        "schema": {
          "type": "string"
        },
        "example": "120;w=60"
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying. Sent only on 429.",
        "schema": {
          "type": "integer"
        },
        "example": 214
      },
      "Idempotency-Replayed": {
        "description": "Present, with the value `true`, when this response was replayed from an earlier request that carried the same `Idempotency-Key` rather than executed again.",
        "schema": {
          "type": "string",
          "enum": ["true"]
        }
      }
    },
    "securitySchemes": {
      "BearerAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "Firebase ID token from `/auth` or any paid route, sent as a Bearer token: `Authorization: Bearer <id_token>` (the `Bearer ` prefix is required)."
      },
      "siwx": {
        "type": "apiKey",
        "in": "header",
        "name": "SIGN-IN-WITH-X",
        "description": "Sign-In-With-X (CAIP-122) wallet signature proving ownership of the calling EVM (Base) or Solana wallet. Identity only, no payment. Build it with `@x402/extensions/sign-in-with-x` (`wrapFetchWithSIWx`)."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Optional client-generated unique key (a UUID works) that makes this write safe to retry. A retry with the same key within 24 hours returns the recorded response of the original request, marked with an `Idempotency-Replayed: true` header, instead of performing the write again. Reusing a key with a different body or route is rejected with 422 (`code: idempotency_key_reused`); a retry that arrives while the original is still running gets 409 (`code: idempotency_key_in_progress`), so wait a moment and retry with the same key. Keys are scoped to the authenticated account."
      }
    }
  }
}
