openapi: 3.1.0 info: title: Portuna Sweep API version: "1" description: | Portuna Sweep, the API for crypto processing: sweeps of tokens and the native coin from your deposit addresses (EOAs) to your hot wallets, with no gas on the deposit addresses. A deposit address delegates once, with an EIP-7702 authorization, to the delegate contract of your hot wallet; from then on your gas wallet pays the gas for whole batches of addresses. - Every request except `GET /healthz` and `GET /v1/openapi.yaml` carries `Authorization: Bearer `. A missing, unknown or revoked key gets `401 unauthorized`; a disabled account gets `403 partner_disabled` on every request. - Request bodies are JSON, up to 64 KiB. Unknown fields are refused with `400 invalid_json`. - Amounts are strings with an integer in the smallest units: wei for the native coin, 10⁻⁶ for USDT on Ethereum. - Addresses are `0x`-hex in any case; responses checksum them (EIP-55). Times are RFC 3339, in UTC. - Optional fields without a value are left out of responses. - Errors are `{"error": {"code", "message"}}`: `code` is stable, `message` is for people and may change. Any request can also get `500 internal`: retry it later, with the same `external_id` for sweeps and revocations. - The `reason` of a sweep or a revocation and the `error` of a hot wallet are stable codes too, listed with each field. New codes may come: treat one you do not know like `internal`. - Lists come newest first, in pages: up to `limit` rows (50 by default, at most 200) and a `next_cursor` to pass as `cursor` for the next page; on the last page it is `null`. The cursor is opaque. Rows added between requests neither shift the pages nor repeat. Guides, SDKs and code samples are in the Portuna developer documentation. security: - apiKey: [] paths: /healthz: get: operationId: healthCheck summary: Health check description: Answers `204` while the service is up. Needs no API key. security: [] responses: "204": description: The service is up. /v1/openapi.yaml: get: operationId: getOpenAPI summary: OpenAPI specification description: This specification, as the running service knows it. Needs no API key. security: [] responses: "200": description: OpenAPI 3.1 in YAML. content: application/yaml: {} /v1/balance: get: operationId: getBalance summary: Get the balance description: | Your gas wallet and its balance and fees on each chain. The gas wallet is one address on every chain. Fees due (`fees_due`) stay on it in reserve and are not spent on gas; `available` is what is left for gas. responses: "200": description: The balance on each chain. content: application/json: schema: { $ref: "#/components/schemas/Balance" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/tariff: get: operationId: getTariff summary: Get the tariff description: | Your rates on each chain and the gas benchmarks your fees are computed with. The fee for a token sweep is max(0; X × classic benchmark − gas of the sweep), where X is `rate_bps`. For a native coin sweep it is a markup on the sweep's gas, `native_markup_bps`, but never more than the sweep saves against a plain 21,000-gas transfer. Everything is counted in gas, at the batch's gas price. responses: "200": description: The tariff on each chain. content: application/json: schema: type: object required: [chains] properties: chains: type: array items: { $ref: "#/components/schemas/ChainTariff" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/settings: get: operationId: getSettings summary: Get the settings responses: "200": description: Your settings. content: application/json: schema: { $ref: "#/components/schemas/Settings" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } patch: operationId: updateSettings summary: Update the settings description: Changes only the fields in the request and keeps the others. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/Settings" } responses: "200": description: The settings after the change. content: application/json: schema: { $ref: "#/components/schemas/Settings" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/hot-wallets: get: operationId: listHotWallets summary: List hot wallets responses: "200": description: Your hot wallets on every chain. content: application/json: schema: type: object required: [hot_wallets] properties: hot_wallets: type: array items: { $ref: "#/components/schemas/HotWallet" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: operationId: createHotWallet summary: Register a hot wallet description: | Registers a hot wallet on a chain and deploys its delegate, the contract your deposit addresses sign their authorizations for. Its address, in `delegate`, is known at once. When the delegate is deployed, the hot wallet becomes `active` and you get a `hot_wallet.active` event; if the deployment fails, `hot_wallet.failed`. Registering the same hot wallet again returns it; after `failed` (for example, the gas wallet had no gas) it restarts the deployment. requestBody: required: true content: application/json: schema: type: object required: [chain_id, address] additionalProperties: false properties: chain_id: { $ref: "#/components/schemas/ChainID" } address: $ref: "#/components/schemas/Address" description: The hot wallet, where sweeps will send the funds. responses: "200": description: The hot wallet is already active. content: application/json: schema: { $ref: "#/components/schemas/HotWallet" } "202": description: The delegate is being deployed. content: application/json: schema: { $ref: "#/components/schemas/HotWallet" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": description: | `unknown_chain`: the chain is not supported in this environment; `invalid_address`: `address` is not a non-zero `0x`-address. content: application/json: schema: { $ref: "#/components/schemas/Error" } /v1/sweeps: get: operationId: listSweeps summary: List sweeps description: Your sweeps, newest first. The filters combine. parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" - name: status in: query description: Only sweeps with this status. schema: { type: string, enum: [queued, processing, swept, failed, rejected] } - $ref: "#/components/parameters/ChainIDFilter" - name: account in: query description: Only sweeps from this deposit address. schema: { $ref: "#/components/schemas/Address" } - name: hot_wallet in: query description: Only sweeps to this hot wallet. schema: { $ref: "#/components/schemas/Address" } responses: "200": description: A page of sweeps. content: application/json: schema: type: object required: [items, next_cursor] properties: items: type: array items: { $ref: "#/components/schemas/Sweep" } next_cursor: { $ref: "#/components/schemas/NextCursor" } "400": { $ref: "#/components/responses/InvalidQuery" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: operationId: createSweep summary: Create a sweep description: | Sweeps a token or the native coin from a deposit address to a hot wallet. Sweeps of one hot wallet that have arrived by the time a batch is sent go in that one batch. Until the deposit address is delegated, the request carries its `authorization`. The outcome comes in a `sweep.swept`, `sweep.failed` or `sweep.rejected` webhook event and shows in `GET /v1/sweeps/{id}`. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SweepRequest" } responses: "200": description: | A sweep with this `external_id` already exists. It is returned as it is now, and no other sweep is created. content: application/json: schema: { $ref: "#/components/schemas/Sweep" } "202": description: The sweep is queued. content: application/json: schema: { $ref: "#/components/schemas/Sweep" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "409": description: | `hot_wallet_not_active`: the hot wallet's delegate is still being deployed, or its deployment failed. content: application/json: schema: { $ref: "#/components/schemas/Error" } "422": description: | `unknown_chain`, `invalid_external_id`, `invalid_address`, `unknown_token`, `invalid_amount`, `unknown_hot_wallet` (the hot wallet is not registered on this chain) or `invalid_authorization`; `message` says what is wrong. content: application/json: schema: { $ref: "#/components/schemas/Error" } /v1/sweeps/{id}: get: operationId: getSweep summary: Get a sweep parameters: - name: id in: path required: true description: The sweep's `id`. schema: { type: string, format: uuid } responses: "200": description: The sweep. content: application/json: schema: { $ref: "#/components/schemas/Sweep" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /v1/revocations: get: operationId: listRevocations summary: List revocations description: Your revocations, newest first. The filters combine. parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" - name: status in: query description: Only revocations with this status. schema: { type: string, enum: [queued, processing, revoked, failed, rejected] } - $ref: "#/components/parameters/ChainIDFilter" - name: account in: query description: Only revocations of this deposit address. schema: { $ref: "#/components/schemas/Address" } responses: "200": description: A page of revocations. content: application/json: schema: type: object required: [items, next_cursor] properties: items: type: array items: { $ref: "#/components/schemas/Revocation" } next_cursor: { $ref: "#/components/schemas/NextCursor" } "400": { $ref: "#/components/responses/InvalidQuery" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: operationId: createRevocation summary: Revoke a delegation description: | Turns a deposit address back into a plain address. The deposit address signs an authorization of the zero address, and your gas wallet sends it in a transaction to itself. The gas comes from your gas wallet, with no fee. A revocation waits for the sweeps of the same deposit address requested before it. Sign with the deposit address's nonce as it will be when the revocation is sent: if a sweep with an authorization goes on chain first, the nonce grows by 1. The outcome comes in a `revocation.revoked`, `revocation.failed` or `revocation.rejected` webhook event and shows in `GET /v1/revocations/{id}`. To sweep the address again later, it needs a new authorization of the delegate. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/RevocationRequest" } responses: "200": description: | A revocation with this `external_id` already exists. It is returned as it is now, and no other revocation is created. content: application/json: schema: { $ref: "#/components/schemas/Revocation" } "202": description: The revocation is queued. content: application/json: schema: { $ref: "#/components/schemas/Revocation" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": description: | `unknown_chain`, `invalid_external_id`, `invalid_address` or `invalid_authorization` (also when `authorization` is missing); `message` says what is wrong. content: application/json: schema: { $ref: "#/components/schemas/Error" } /v1/revocations/{id}: get: operationId: getRevocation summary: Get a revocation parameters: - name: id in: path required: true description: The revocation's `id`. schema: { type: string, format: uuid } responses: "200": description: The revocation. content: application/json: schema: { $ref: "#/components/schemas/Revocation" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /v1/ledger: get: operationId: listLedgerEntries summary: List gas wallet entries description: | Everything that moved your gas wallet's balance, newest first: the gas of transactions, fees, charges and credits under your agreement, withdrawals of fees and top-ups. Fees (`fee`, `fee_charge`) stay on the wallet in reserve until a `withdrawal` takes them, so a withdrawal is not a new expense: it is that reserve leaving the wallet, together with the withdrawal's gas. parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/ChainIDFilter" - name: kind in: query description: Only entries of this kind. schema: { $ref: "#/components/schemas/LedgerKind" } responses: "200": description: A page of entries. content: application/json: schema: type: object required: [items, next_cursor] properties: items: type: array items: { $ref: "#/components/schemas/LedgerEntry" } next_cursor: { $ref: "#/components/schemas/NextCursor" } "400": { $ref: "#/components/responses/InvalidQuery" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/usage: get: operationId: getUsage summary: Get daily usage description: | Each of the last `days` days, today included, by UTC day, oldest first, on every chain of the environment (or only on `chain_id`), with zeros for days without activity. Sweeps and revocations count on the day they were created, by how they ended; gas counts on the day of its transaction, fees on the day they accrued. parameters: - name: days in: query description: How many days to return, today included. schema: { type: integer, minimum: 1, maximum: 90, default: 30 } - $ref: "#/components/parameters/ChainIDFilter" responses: "200": description: A row for each day and chain. content: application/json: schema: type: object required: [days] properties: days: type: array items: { $ref: "#/components/schemas/UsageDay" } "400": { $ref: "#/components/responses/InvalidQuery" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/webhook: get: operationId: getWebhook summary: Get the webhook responses: "200": description: Your webhook settings. content: application/json: schema: { $ref: "#/components/schemas/Webhook" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NoWebhook" } put: operationId: setWebhook summary: Set the webhook description: | Sets the URL events go to and the low balance alerts of your gas wallet. Only public `https` addresses; redirects are not followed. The response carries the `secret` that signs deliveries: it is created with the webhook and stays the same when the URL changes. `low_balance` replaces all the alerts: chains without a threshold there get no alert, and a request without `low_balance` removes them all. requestBody: required: true content: application/json: schema: type: object required: [url] additionalProperties: false properties: url: type: string format: uri maxLength: 2048 description: Where events go. examples: ["https://processing.example/hooks/sweeps"] low_balance: type: array description: Low balance alerts of your gas wallet, at most one per chain. items: type: object required: [chain_id, threshold] additionalProperties: false properties: chain_id: { $ref: "#/components/schemas/ChainID" } threshold: $ref: "#/components/schemas/Amount" description: Alert when the gas wallet's balance on this chain falls below this, in wei; above zero. responses: "200": description: The webhook is set. content: application/json: schema: { $ref: "#/components/schemas/Webhook" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": description: | `invalid_url`: the URL is not a public `https` URL; `unknown_chain`: a chain in `low_balance` is not supported or is listed twice; `invalid_amount`: a threshold is not an integer above zero. content: application/json: schema: { $ref: "#/components/schemas/Error" } delete: operationId: deleteWebhook summary: Delete the webhook description: | Removes the webhook and its low balance alerts. Events not delivered yet are dropped: they show as `failed` in the delivery log. A webhook set again later gets a new secret. responses: "204": description: Deleted. "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NoWebhook" } /v1/webhook/deliveries: get: operationId: listWebhookDeliveries summary: List webhook deliveries description: Your events, newest first, and how their delivery went. parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: A page of events. content: application/json: schema: type: object required: [items, next_cursor] properties: items: type: array items: { $ref: "#/components/schemas/WebhookDelivery" } next_cursor: { $ref: "#/components/schemas/NextCursor" } "400": { $ref: "#/components/responses/InvalidQuery" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/webhook/test: post: operationId: sendTestWebhook summary: Send a test event description: | Queues a `webhook.test` event. It is signed and delivered like any other event, and shows in the delivery log. responses: "202": description: The event is queued. content: application/json: schema: type: object required: [id] properties: id: type: string format: uuid description: The event's `id`, as in the delivery and in the delivery log. "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NoWebhook" } webhooks: event: post: operationId: receiveEvent summary: Webhook event description: | Each event goes to your webhook URL as a `POST` with a JSON body. Events are recorded only while a webhook is set. - Delivery is at least once: handle each event `id` once. Order is not guaranteed: rely on the status in `data`, not on the order of events. - Verify every delivery: `v1` in `Portuna-Signature` is the HMAC-SHA256, in hex, of the string `.` with your webhook secret as the key, where `` is the raw request body. Accept `t` only within 5 minutes of your clock. Each attempt is signed anew. - Answer with any 2xx within 10 seconds. Any other answer, a redirect included, or no answer in time is a failed attempt: the event is tried again after 10 s, 20 s, 40 s and so on, up to an hour between attempts, 12 attempts in all. - New event types may come: accept and ignore the ones you do not handle. # Deliveries carry no API key: they are signed instead. security: [] parameters: - name: Portuna-Signature in: header required: true description: | `t=,v1=`: the time of this attempt and the HMAC-SHA256 of `.` with your webhook secret, in hex. schema: { type: string, examples: ["t=1791634982,v1=6e735f32a0…"] } - name: Portuna-Event in: header required: true description: The event type, as in `type` in the body. schema: { type: string, examples: ["sweep.swept"] } - name: Portuna-Delivery in: header required: true description: The event's `id`, as in the body. It is the same in every attempt. schema: { type: string, format: uuid } - name: User-Agent in: header description: Names the sender. Verify deliveries by their signature, not by this header. schema: { type: string, examples: ["portuna-webhooks/1"] } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/Event" } responses: "2XX": description: The event is accepted; any 2xx will do, and the body is ignored. components: securitySchemes: apiKey: type: http scheme: bearer description: | Your API key, created in the Portuna console. A test key (testnets) starts with `ptn_test_`, a live key (mainnet) with `ptn_live_`. responses: BadRequest: description: "`invalid_json`: the body is not JSON, is over 64 KiB or has unknown fields." content: application/json: schema: { $ref: "#/components/schemas/Error" } Unauthorized: description: "`unauthorized`: no API key, an unknown one or a revoked one." content: application/json: schema: { $ref: "#/components/schemas/Error" } Forbidden: description: "`partner_disabled`: your account is disabled." content: application/json: schema: { $ref: "#/components/schemas/Error" } NotFound: description: "`not_found`: there is no such object in your account." content: application/json: schema: { $ref: "#/components/schemas/Error" } NoWebhook: description: "`not_found`: no webhook is set." content: application/json: schema: { $ref: "#/components/schemas/Error" } InvalidQuery: description: "`invalid_query`: a query parameter is not valid; `message` says which." content: application/json: schema: { $ref: "#/components/schemas/Error" } parameters: Limit: name: limit in: query description: How many rows a page holds. schema: { type: integer, minimum: 1, maximum: 200, default: 50 } Cursor: name: cursor in: query description: The `next_cursor` of the previous page; none for the first page. schema: { type: string } ChainIDFilter: name: chain_id in: query description: Only this chain. schema: { $ref: "#/components/schemas/ChainID" } schemas: Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string description: What went wrong. Stable, so branch on it. examples: ["invalid_authorization"] message: type: string description: The details, for people. It may change. Address: type: string pattern: "^0x[0-9a-fA-F]{40}$" description: A `0x`-hex address, in any case in requests and checksummed (EIP-55) in responses. examples: ["0x2Cd50979a8A33e8CA37DA85F8517fb148a67b769"] Amount: type: string pattern: "^[0-9]+$" description: An integer in the smallest units, as a string. examples: ["1000000"] ChainID: type: integer format: int64 minimum: 1 description: | The chain's id (EIP-155): `11155111` Sepolia and `97` BSC testnet with a test key, `1` Ethereum and `56` BSC with a live key. examples: [11155111, 97] Gas: type: integer format: int64 minimum: 0 description: An amount of gas. NextCursor: type: [string, "null"] description: The cursor of the next page; `null` on the last page. LedgerKind: type: string enum: [gas, fee, fee_charge, fee_credit, withdrawal, topup] description: | `gas`: the gas of a transaction of your gas wallet; `fee`: the fee for a sweep; `fee_charge`: a charge made under your agreement; `fee_credit`: a credit made under your agreement, which reduces the fees due; `withdrawal`: fees withdrawn from the gas wallet, together with the withdrawal's gas; `topup`: a top-up of the gas wallet, found by the balance reconciliation. LedgerEntry: type: object description: A change of your gas wallet's balance, or of the fees it holds. required: [id, chain_id, kind, amount, tx_hash, sweep_id, note, created_at] properties: id: { type: integer, format: int64 } chain_id: { $ref: "#/components/schemas/ChainID" } kind: { $ref: "#/components/schemas/LedgerKind" } amount: type: string pattern: "^-?[0-9]+$" description: | Wei with a sign: plus for `topup` and `fee_credit`, minus for gas, fees, charges and withdrawals. examples: ["-1250000000000000"] tx_hash: type: [string, "null"] description: The transaction of the entry; `null` for top-ups, charges and credits. sweep_id: type: [string, "null"] format: uuid description: The sweep a `fee` is for; `null` for other kinds. note: type: [string, "null"] description: A note on charges and credits; for top-ups, the blocks the reconciliation covered. created_at: { type: string, format: date-time } UsageDay: type: object description: Your activity on one chain in one UTC day. required: [date, chain_id, sweeps_done, sweeps_failed, sweeps_rejected, revocations_done, gas_spent, fees] properties: date: { type: string, format: date, description: The UTC day., examples: ["2026-10-10"] } chain_id: { $ref: "#/components/schemas/ChainID" } sweeps_done: type: integer description: Sweeps created that day that ended `swept`. sweeps_failed: type: integer description: Sweeps created that day that ended `failed`, having reverted on chain or run out of attempts. sweeps_rejected: type: integer description: | Sweeps created that day that ended `rejected`: they were never sent, because a check before sending kept failing. revocations_done: type: integer description: Revocations created that day that ended `revoked`. gas_spent: $ref: "#/components/schemas/Amount" description: The gas of your gas wallet's transactions that day, in wei, fee withdrawals aside. fees: type: string pattern: "^-?[0-9]+$" description: | Fees and charges accrued that day minus credits, in wei; below zero when the credits exceed them. WebhookDelivery: type: object description: A webhook event and how its delivery went. required: [id, event, status, attempts, last_status_code, last_error, created_at, delivered_at, next_attempt_at] properties: id: type: string format: uuid description: The event's `id`, as in the delivery. event: type: string description: The event type. examples: ["sweep.swept"] status: type: string enum: [pending, delivered, failed] description: | `pending`: being delivered, or waiting for the next attempt; `delivered`: your endpoint answered with a 2xx; `failed`: the attempts ran out, or the webhook was deleted. attempts: type: integer description: Attempts made so far. last_status_code: type: [integer, "null"] description: The HTTP status your endpoint answered the last attempt with; `null` if it did not answer. last_error: type: [string, "null"] description: | Why the last attempt failed; `null` if it did not. `status `: your endpoint answered with another status. `timeout`, `connection refused`, `connection reset`, `connection closed`, `DNS lookup failed`, `TLS error`, `connection failed`: it did not answer; `private address refused`: its host resolves to an address that is not public. `webhook removed`: the webhook was deleted. `internal`: a failure on our side. created_at: type: string format: date-time description: When the event happened. delivered_at: type: [string, "null"] format: date-time description: When the event was delivered; `null` until then. next_attempt_at: type: [string, "null"] format: date-time description: When the next attempt is due; only for `pending`. Balance: type: object description: Your gas wallet and its balance on each chain. required: [gas_wallet, chains] properties: gas_wallet: $ref: "#/components/schemas/Address" description: Your gas wallet, the same address on every chain. chains: type: array items: type: object required: [chain_id, name, native, fees_accrued, fees_withdrawn, fees_due] properties: chain_id: { $ref: "#/components/schemas/ChainID" } name: type: string description: The chain's name. examples: ["sepolia"] native: type: string description: The ticker of the chain's native coin. examples: ["ETH"] balance: $ref: "#/components/schemas/Amount" description: The gas wallet's balance, in wei; absent when `error` is set. fees_accrued: $ref: "#/components/schemas/Amount" description: All fees and charges accrued on this chain minus credits, in wei. fees_withdrawn: $ref: "#/components/schemas/Amount" description: Fees withdrawn so far, with the gas of their withdrawals, in wei. fees_due: $ref: "#/components/schemas/Amount" description: Fees accrued and not withdrawn yet, in wei. They stay on the gas wallet in reserve. available: $ref: "#/components/schemas/Amount" description: | What is left for gas, in wei: `balance` minus `fees_due`, but not below zero. Absent when `error` is set. error: type: string enum: [chain_unavailable] description: "`chain_unavailable`: the chain's node did not answer, so there is no balance." ChainTariff: type: object description: Your rates and the gas benchmarks on one chain. required: [chain_id, name, rate_bps, rate, native_markup_bps, native_markup] properties: chain_id: { $ref: "#/components/schemas/ChainID" } name: type: string description: The chain's name. examples: ["sepolia"] rate_bps: type: integer description: X for token sweeps, in basis points, 7000 = 70%; 0 means no fee. rate: type: string description: "`rate_bps` in percent." examples: ["70%"] native_markup_bps: type: integer description: The markup on gas for native coin sweeps, in basis points. native_markup: type: string description: "`native_markup_bps` in percent." examples: ["10%"] classic_gas: type: object description: | The classic benchmark of each asset, by the token's ticker or the native coin's: the gas of topping up a deposit address and transferring from it. Absent on a chain without benchmarks. additionalProperties: type: object required: [new, existing] properties: new: $ref: "#/components/schemas/Gas" description: Gas for an address without an account on the chain, which the top-up creates (tokens only). existing: $ref: "#/components/schemas/Gas" description: Gas for an address with an account. extra_gas: type: object description: | The batch gas a first sweep of an address bears on top of a repeat one. Absent on a chain without benchmarks. required: [delegation, account] properties: delegation: $ref: "#/components/schemas/Gas" description: Setting the address's delegate by its authorization. account: $ref: "#/components/schemas/Gas" description: Creating the address's account as well, for an address that held only tokens. Settings: type: object additionalProperties: false properties: keep_one_unit: type: boolean description: | Token sweeps leave 1 smallest unit on the deposit address, which makes your user's next deposit cheaper. A sweep request can override it. Off by default. HotWallet: type: object description: A hot wallet on one chain and its delegate. required: [chain_id, address, delegate, status, created_at] properties: chain_id: { $ref: "#/components/schemas/ChainID" } address: $ref: "#/components/schemas/Address" description: The hot wallet, where sweeps send the funds. delegate: $ref: "#/components/schemas/Address" description: The hot wallet's delegate, the address deposit addresses sign their authorizations for. status: type: string enum: [pending, active, failed] description: | `pending`: the delegate is being deployed; `active`: it is deployed, and sweeps can go; `failed`: the deployment failed, and registering the hot wallet again retries it. error: type: string enum: [not_deployed, insufficient_gas_balance, gas_limit_exceeded, transaction_reverted, nonce_conflict, chain_unavailable, internal] description: | Why the deployment failed; only with `failed`. Most often `insufficient_gas_balance`: top up the gas wallet and register the hot wallet again. `not_deployed`: the deployment went through, but the delegate is not there. The other codes are the causes of a failed attempt, as in a sweep's `reason`. tx_hash: type: string description: The transaction that deployed the delegate. created_at: type: string format: date-time description: When the hot wallet was registered. Authorization: type: object description: | An EIP-7702 authorization signed by the deposit address's key: on chain `chain_id`, the deposit address runs the code at `address`. How to sign it, also in an HSM, a KMS or an MPC wallet: the Signing authorizations guide in the developer documentation. required: [chain_id, address, nonce, y_parity, r, s] additionalProperties: false properties: chain_id: type: integer format: int64 minimum: 1 description: The chain of the request; 0 (any chain) is refused. address: $ref: "#/components/schemas/Address" description: The hot wallet's delegate from `POST /v1/hot-wallets`; the zero address for a revocation. nonce: type: integer format: int64 minimum: 0 description: The deposit address's current nonce. y_parity: type: integer enum: [0, 1] description: The signature's y parity. r: type: string pattern: "^0x[0-9a-fA-F]{1,64}$" description: The signature's r, `0x`-hex of up to 32 bytes. s: type: string pattern: "^0x[0-9a-fA-F]{1,64}$" description: The signature's s, `0x`-hex of up to 32 bytes, in the lower half of the curve order (EIP-2). SweepRequest: type: object description: What to sweep, from where and to where. required: [chain_id, hot_wallet, account, token] additionalProperties: false properties: external_id: type: string maxLength: 128 description: | Your id for the sweep, unique in your account: a request with an `external_id` that is already taken returns that sweep instead of creating another, so retries are safe. chain_id: { $ref: "#/components/schemas/ChainID" } hot_wallet: $ref: "#/components/schemas/Address" description: Your hot wallet on this chain; its delegate must be `active`. account: $ref: "#/components/schemas/Address" description: The deposit address. token: type: string description: | A token's ticker listed on the chain (in any case), the contract address of a listed token, or `native` (or the native coin's ticker) for the native coin. examples: ["USDT", "native"] amount: $ref: "#/components/schemas/Amount" description: How much to sweep, above zero; without it, the whole balance. keep_one_unit: type: boolean description: | Leave 1 smallest unit of the token on the deposit address; defaults to your settings. Ignored for the native coin. authorization: $ref: "#/components/schemas/Authorization" description: | Needed until the deposit address is delegated to this hot wallet's delegate. After that it is not used, though one that is sent must still be valid. Sweep: type: object description: A sweep and its outcome. required: [id, chain_id, hot_wallet, account, token, status, attempts, created_at, updated_at] properties: id: { type: string, format: uuid } external_id: type: string description: Your id for the sweep, if the request had one. chain_id: { $ref: "#/components/schemas/ChainID" } hot_wallet: $ref: "#/components/schemas/Address" description: The hot wallet. account: $ref: "#/components/schemas/Address" description: The deposit address. token: type: string description: The token's ticker, or the native coin's. examples: ["USDT"] token_address: $ref: "#/components/schemas/Address" description: The token's contract address; absent for the native coin. amount: $ref: "#/components/schemas/Amount" description: The amount requested; absent when the request was for the whole balance. keep_one_unit: type: boolean description: Present and `true` when the sweep leaves 1 smallest unit of the token on the deposit address. status: type: string enum: [queued, processing, swept, failed, rejected] description: | `queued`: waiting for a batch or a retry; `processing`: in a batch being prepared, sent or confirmed; `swept`: done, the funds are in the hot wallet; `failed`: reverted on chain, or ran out of attempts; `rejected`: never sent. The last three are final. reason: type: string enum: [not_upgraded, stale_authorization, would_fail, nothing_to_sweep, reverted, TokenTransferFailed, NativeTransferFailed, NothingToSweep, Unauthorized, insufficient_gas_balance, gas_limit_exceeded, transaction_reverted, nonce_conflict, chain_unavailable, internal] description: | Why the sweep ended `rejected` or `failed`, or, while it is `queued` for a retry, why the last attempt did not go through. A stable code: the node's own messages stay in our logs. - `rejected`, a check before sending kept failing: `not_upgraded`, `stale_authorization`, `would_fail` or `nothing_to_sweep`. - `failed` on chain, inside a batch that went through: the contract error the transfer reverted with, `TokenTransferFailed`, `NativeTransferFailed`, `NothingToSweep` or `Unauthorized`, or `reverted` without one. - The cause of a failed attempt, which is retried, and final with `failed` once the attempts run out: `insufficient_gas_balance` (top up the gas wallet), `gas_limit_exceeded`, `transaction_reverted`, `nonce_conflict`, `chain_unavailable` or `internal`. attempts: type: integer description: How many times the sweep was tried. swept_amount: $ref: "#/components/schemas/Amount" description: How much reached the hot wallet; only with `swept`. tx_hash: type: string description: The batch transaction that carried the sweep. kind: type: string enum: [repeat, first, first_new] description: | What the deposit address needed in the batch, which decides its share of the gas and the benchmark. `repeat`: nothing, it was already delegated. `first`: its delegation; it already had an account on the chain. `first_new`: its delegation, which also created its account, as it held only tokens. gas: $ref: "#/components/schemas/Gas" description: The sweep's share of the batch gas. gas_cost: $ref: "#/components/schemas/Amount" description: The same in wei. fee: $ref: "#/components/schemas/Amount" description: The fee in wei; only with `swept`. classic_gas: $ref: "#/components/schemas/Gas" description: The benchmark the fee was computed with. rate_bps: type: integer description: The rate the fee was computed with; for the native coin, the markup on gas. created_at: type: string format: date-time description: When the sweep was requested. updated_at: type: string format: date-time description: When the sweep last changed. RevocationRequest: type: object description: Which deposit address to revoke, with its authorization of the zero address. required: [chain_id, account, authorization] additionalProperties: false properties: external_id: type: string maxLength: 128 description: | Your id for the revocation, unique in your account: a request with an `external_id` that is already taken returns that revocation instead of creating another, so retries are safe. chain_id: { $ref: "#/components/schemas/ChainID" } account: $ref: "#/components/schemas/Address" description: The deposit address. authorization: $ref: "#/components/schemas/Authorization" description: The deposit address's authorization with `address` = `0x0000000000000000000000000000000000000000`. Revocation: type: object description: A revocation and its outcome. required: [id, chain_id, account, status, attempts, created_at, updated_at] properties: id: { type: string, format: uuid } external_id: type: string description: Your id for the revocation, if the request had one. chain_id: { $ref: "#/components/schemas/ChainID" } account: $ref: "#/components/schemas/Address" description: The deposit address. status: type: string enum: [queued, processing, revoked, failed, rejected] description: | `queued` and `processing`: as for a sweep; `revoked`: the deposit address has no delegation any more; `failed`: the transaction went through but the chain skipped the authorization, or the attempts ran out; `rejected`: never sent. The last three are final. reason: type: string enum: [not_delegated, stale_authorization, invalid_authorization, not_applied, insufficient_gas_balance, gas_limit_exceeded, transaction_reverted, nonce_conflict, chain_unavailable, internal] description: | Why the revocation ended `rejected` or `failed`, or, while it is `queued` for a retry, why the last attempt did not go through. For `rejected`: `not_delegated` (there is no delegation to remove), `stale_authorization` (the deposit address's nonce has changed) or `invalid_authorization`. For `failed`: `not_applied` (the chain skipped the authorization). The other codes are the causes of a failed attempt, as in a sweep's `reason`. attempts: type: integer description: How many times the revocation was tried. tx_hash: type: string description: The transaction that carried the revocation. gas: $ref: "#/components/schemas/Gas" description: The revocation's share of its transaction's gas. gas_cost: $ref: "#/components/schemas/Amount" description: The same in wei. created_at: type: string format: date-time description: When the revocation was requested. updated_at: type: string format: date-time description: When the revocation last changed. Webhook: type: object description: Your webhook settings. required: [url, secret, low_balance] properties: url: type: string format: uri description: Where events go. secret: type: string description: The signing secret, `whsec_…`. Keep it on your backend. low_balance: type: array description: Low balance alerts of your gas wallet, one per chain. items: type: object required: [chain_id, threshold, low] properties: chain_id: { $ref: "#/components/schemas/ChainID" } threshold: $ref: "#/components/schemas/Amount" description: The threshold, in wei. low: type: boolean description: The balance is below the threshold, and the alert was sent; it re-arms once the balance recovers. Event: description: | The body of a webhook delivery. `type` tells what happened and what `data` is: a sweep as in `GET /v1/sweeps/{id}`, a revocation as in `GET /v1/revocations/{id}`, a hot wallet as in `GET /v1/hot-wallets`, a low balance alert or a test event. Sweeps, revocations and hot wallets are shown as they are when the event is delivered. oneOf: - $ref: "#/components/schemas/SweepEvent" - $ref: "#/components/schemas/RevocationEvent" - $ref: "#/components/schemas/HotWalletEvent" - $ref: "#/components/schemas/BalanceLowEvent" - $ref: "#/components/schemas/WebhookTestEvent" discriminator: propertyName: type mapping: sweep.swept: "#/components/schemas/SweepEvent" sweep.failed: "#/components/schemas/SweepEvent" sweep.rejected: "#/components/schemas/SweepEvent" revocation.revoked: "#/components/schemas/RevocationEvent" revocation.failed: "#/components/schemas/RevocationEvent" revocation.rejected: "#/components/schemas/RevocationEvent" hot_wallet.active: "#/components/schemas/HotWalletEvent" hot_wallet.failed: "#/components/schemas/HotWalletEvent" balance.low: "#/components/schemas/BalanceLowEvent" webhook.test: "#/components/schemas/WebhookTestEvent" SweepEvent: type: object description: | A sweep ended. `sweep.swept`: it moved the funds; `sweep.failed`: it reverted on chain or ran out of attempts; `sweep.rejected`: it was never sent. required: [id, type, created_at, data] properties: id: type: string format: uuid description: The event's id, the same in every attempt. type: type: string enum: [sweep.swept, sweep.failed, sweep.rejected] description: The event type. created_at: type: string format: date-time description: When the event happened. data: { $ref: "#/components/schemas/Sweep" } RevocationEvent: type: object description: | A revocation ended. `revocation.revoked`: the delegation is removed; `revocation.failed`: the chain skipped the authorization, or the attempts ran out; `revocation.rejected`: it was never sent. required: [id, type, created_at, data] properties: id: type: string format: uuid description: The event's id, the same in every attempt. type: type: string enum: [revocation.revoked, revocation.failed, revocation.rejected] description: The event type. created_at: type: string format: date-time description: When the event happened. data: { $ref: "#/components/schemas/Revocation" } HotWalletEvent: type: object description: | A hot wallet's delegate deployment ended. `hot_wallet.active`: the delegate is deployed; `hot_wallet.failed`: the deployment failed. required: [id, type, created_at, data] properties: id: type: string format: uuid description: The event's id, the same in every attempt. type: type: string enum: [hot_wallet.active, hot_wallet.failed] description: The event type. created_at: type: string format: date-time description: When the event happened. data: { $ref: "#/components/schemas/HotWallet" } BalanceLowEvent: type: object description: | Your gas wallet fell below a low balance threshold. One event each time it falls below; the alert re-arms once the balance recovers. required: [id, type, created_at, data] properties: id: type: string format: uuid description: The event's id, the same in every attempt. type: type: string enum: [balance.low] description: The event type. created_at: type: string format: date-time description: When the event happened. data: { $ref: "#/components/schemas/BalanceLow" } WebhookTestEvent: type: object description: The test event you asked for with `POST /v1/webhook/test`. required: [id, type, created_at, data] properties: id: type: string format: uuid description: The event's id, the same in every attempt. type: type: string enum: [webhook.test] description: The event type. created_at: type: string format: date-time description: When the event happened. data: { $ref: "#/components/schemas/WebhookTest" } BalanceLow: type: object description: A low balance alert. required: [chain_id, gas_wallet, balance, threshold] properties: chain_id: { $ref: "#/components/schemas/ChainID" } gas_wallet: $ref: "#/components/schemas/Address" description: Your gas wallet. balance: $ref: "#/components/schemas/Amount" description: Its balance on this chain when the alert fired, in wei. threshold: $ref: "#/components/schemas/Amount" description: The threshold it fell below, in wei. WebhookTest: type: object description: The data of a test event. required: [message] properties: message: { type: string, examples: ["Test event: the webhook works."] }