CoinCircuit API

Introduction

Sandbox Environment

MCP Integration

Official SDKs

Authentication

Webhooks

Error Handling

Rate Limiting

Pagination

Invoices

Overview

Standard headers

Event identification

Automatic retries

Responding

What CoinCircuit handles

Base URLs

Next steps

Changelog

v2.3.0

Deposit Accounts, Balance Ledger & Webhooks

Deposit Accounts

Balance Transactions

Deposit & Swap Webhooks

Deposits

v2.2.0

Swap for On/Off Ramping

Swap Endpoints

v2.1.0

x402 Agent Payments, Base and Arbitrum

Agent Payment Endpoints

New Supported Blockchains

v2.0.0

Payment API Transition and Payouts Unified

Payment API Transition

Payouts API Deprecation & Migration

Migration Guide

Generate a new API key

Update webhook event type handlers

Update payment.partial handler

Update session response parsing

Migrate payout endpoints

Update payout amount handling

Update payout response parsing

Welcome to CoinCircuit, crypto payment infrastructure for online payments, in-store payments, payouts, and refunds through a single API.

New here? Start with the Quick Start for a guided walkthrough.

Need help? Use the AI assistant in the bottom-right corner for guidance while navigating the documentation.

Use sandbox to test your integration safely before going live.

Use sandbox API keys (test keys) against the sandbox base URL, and live keys against production.

CoinCircuit provides a Model Context Protocol (MCP) server for seamless integration with AI-powered IDEs like Cursor, Windsurf, Claude Desktop, and more.

MCP URL: https://mcp.coincircuit.io

Generate an MCP access key from your CoinCircuit dashboard under Developer > MCP Access Keys, then add it to your config:

The MCP server works without authentication for documentation browsing and API schema discovery. Authentication is only required to execute tools (create payments, list transactions, etc.).

Install the CoinCircuit SDK for your language.

Node.js / TypeScript

Checkout SDK

npm | PyPI | Checkout SDK

Authenticate requests by including your API key in the x-api-key header.

CoinCircuit sends webhook events as payments progress. See the Webhooks tab in this page for detailed integration guidance.

The API uses standard HTTP status codes to indicate the success or failure of a request.

Basic Error Example:

The API is rate-limited to 20 requests per second. Exceeding this limit will result in a 429 Too Many Requests response.

List endpoints support pagination using page (default 1) and size (default 10, max 100) query parameters.

Response Metadata Example:

Production Server

Create and manage invoices

How CoinCircuit delivers webhook events, with headers, retries, and idempotency.

CoinCircuit sends webhook events to your endpoint as payments, payouts, refunds, deposits, and other resources change state. Each event is a POST request with a JSON body.

Every delivery carries these headers.

Every event includes a unique X-CoinCircuit-Delivery-Id. Store the ids you have processed and skip repeats so the same event is never handled twice.

Failed deliveries (any non-200 response) are retried with exponential backoff, roughly at 0m, 1m, 10m, then 1h. Return a 200 quickly to acknowledge receipt.

Acknowledge with a 2xx status as soon as you have stored the event, then do any heavy work asynchronously so the delivery does not time out.

Accept crypto payments, send payouts, and issue refunds through one API.

Welcome to CoinCircuit, crypto payment infrastructure for online payments, in-store payments, payouts, and refunds, all through a single API.

Open the Quick Start to accept your first payment, or set up the Sandbox to test safely before going live.

API changes, new features, migration guides, and deprecation notices, newest first.

Deposit accounts you can issue per customer, a balance API that returns all your currencies in one call, a balance transactions ledger to inspect every entry that moved your balance, and new deposit and swap webhooks.

Issue a deposit account per customer (or a parent account for your own balance) and provision its pay-in identities: static crypto deposit addresses and NGN virtual accounts. A deposit settles into the account it was issued for.

Create a deposit account for each of your customers, or a single parent account for your own balance. A deposit account is the container that owns its pay-in identities (static crypto addresses and NGN virtual accounts) and is what a deposit settles into. Pass an existing customerId; omit it to create your parent account.

Add a pay-in identity to a deposit account: a static crypto deposit address or an NGN virtual account. Funds sent to any identity credit the account holder. One endpoint serves both; the type field selects which identity to issue.

Fetch a single account with its identities, the parent account, or a customer's account, and list all accounts filtered by status.

A ledger feed of every entry that moved your balance, with rich source linkage and per-entry before/after balances.

A paginated ledger of every entry that moved your balance: deposits, payments, payouts, swaps, refunds, and fees. Filter by source, status, direction, currency, txHash, and date range.

source tells you what created the entry. sourceReference is the reference you pass to that object's API (set for sessions, invoices, and payouts). sourceId is the id of the source object (set for sessions, invoices, payouts, swaps, and deposits). A deposit entry's sourceId is the deposit id, which you can fetch at GET /deposits/:id.

deposit webhooks notify you when a deposit is recorded, credited, or fails, and swap.completed / swap.failed webhooks notify you when a swap finishes.

Get notified about deposits to your balance. deposit.processing fires when a deposit is detected and is processing. deposit.completed fires when a deposit is completed and credited to your balance. deposit.failed fires when a deposit fails a compliance check and is rejected. The payload is a deposit object with the on-chain detail under crypto, or the bank transfer detail under fiat.

Get notified when a balance swap finishes. swap.completed fires on a successful conversion; swap.failed includes a failureReason. Useful when you kick off swaps and want to react without polling.

Deposits are listed and fetched at /deposits, and the deposit webhooks were renamed.

The balance_deposit.processing and balance_deposit.completed events are renamed to deposit.processing and deposit.completed, and a new deposit.failed event was added. The payload is a deposit object with on-chain detail under crypto and bank transfer detail under fiat.

Inbound deposits (crypto and bank transfer) are listed and fetched under a single resource. Each deposit carries a depositAccountId linking it to the deposit account it settled into.

New swap endpoints to convert between USDT and NGN directly from your balance. Supports instant on-ramping (NGN to USDT) and off-ramping (USDT to NGN) with rate locking and atomic execution.

A quotation-based swap flow: estimate the conversion, lock the rate for 15 seconds, then execute atomically. Supports USDT/NGN in both directions with per-swap limits of 30,000 USDT or 50,000,000 NGN.

Lock a swap rate for 15 seconds. Returns a quotation with the guaranteed rate, source and target amounts. Execute within the TTL to swap at the quoted price.

Execute a previously created quotation. Debits the source currency and credits the target currency atomically at the locked rate. Returns the completed swap details.

Preview swap output without locking a rate. Use this to show users an estimated conversion before they commit. The rate is indicative and may change.

Retrieve a quotation by ID. Includes an executed flag indicating whether the quotation has been used. Use expiresAt to determine if the quotation is still valid.

New endpoints for AI agent payments via the x402 protocol, plus Base and Arbitrum added as supported blockchains for all payment sessions.

Two new endpoints enable AI agents to pay for API resources using gasless stablecoin transfers. Supports EIP-3009, Permit2, and Solana signing schemes. CoinCircuit handles on-chain submission and gas fees.

Settle gasless stablecoin payments from AI agents. Accepts signed payloads from three schemes: EIP-3009 (USDC on Base, Arbitrum), Permit2 (USDC, USDT on Base, Arbitrum, BSC), and Solana native fee payer abstraction. CoinCircuit submits the transaction on-chain and covers gas.

Pre-validate a signed agent payment without touching the blockchain. Runs scheme-specific checks: authorization timestamps and nonce replay for EIP-3009, deadline and balance for Permit2, transaction validity for Solana. Returns 200 if the payment will succeed, or 400 with per-check pass/fail results.

Payment sessions can now be used in the x402 flow. Create a session via POST /payments, return the deposit details to the agent as an HTTP 402 response, receive the signed payload back, and settle via the new /payments/agent/settle endpoint. The agent only needs a wallet and signing key.

EIP-3009 for USDC on Base and Arbitrum (native transferWithAuthorization). Permit2 for USDC, USDT, and any ERC-20 on Base, Arbitrum, and BSC (Uniswap universal approval). Solana native fee payer abstraction for SOL, USDC, and USDT. All schemes are gasless for the agent.

Base (Coinbase L2) and Arbitrum are now fully supported for all payment sessions, settlements, and agent payments.

Coinbase's Layer 2 blockchain is now available for all payment sessions. USDC on Base settles in under 2 seconds. Base is the default blockchain for EIP-3009 gasless agent payments.

Arbitrum One is now available for all payment sessions. Supports USDC and USDT via both EIP-3009 and Permit2 signing schemes with sub-second finality.

Major updates to the Payment API and Payouts system, introducing new payload shapes, unified endpoints, and better error handling.

Updated session response shape, renamed webhook event types (session.* → payment.*), and a restructured amount object with inline settlements. Existing API keys are unaffected. New API keys automatically use the updated format.

Payment session webhook event types have been renamed. Previous keys used session.* prefixes; new keys use payment.* prefixes. The webhook payload shape (data.session.*) is unchanged — only the event type string differs. This applies to all payment session outcome events.

Versioning is tied to your API key. New API keys created in the dashboard automatically use the updated response format and new webhook event types. Existing API keys continue to receive the current responses and event types — no action required to preserve your current integration.

The session API response has been restructured. The top-level status field is replaced by state (open/closed). Amount and currency are flat strings instead of a nested object. The financial breakdown (gross, fees, net) moves to a separate settlements object.

The top-level status field is now state — "open" while active and awaiting payment, "closed" once finalized. The payment outcome is available in payment.status.

Payment session responses now include a settlements object inline with the settlement currency, gross amount with conversion rate, fee breakdown (processing + gas), and net amount. No need to call the settlements endpoint separately.

The legacy payout endpoints (/payout/fiat, /payout/crypto) are deprecated and will be removed on March 30, 2026. The new /payouts endpoint unifies fiat and crypto payouts into a single request.

The existing /payout/fiat and /payout/crypto endpoints are deprecated and will be removed on March 30, 2026. Migrate to the new unified POST /payouts endpoint. Separate fee endpoints (/payout/fiat/fee and /payout/crypto/fee) are replaced by GET /payouts/fees.

A single POST /payouts endpoint handles both fiat and crypto payouts. Specify the method in the request body. The bankAccountId and addressId fields are replaced by a single recipientId field.

In the old API, amount was the total debited from your balance (including fees) — the recipient got less. In the new API, amount is exactly what the recipient receives. Fees are charged separately on top. No need to add fees to your amount.

The response replaces the nested amount object and separate bankAccount/crypto objects with flat amount/fee/total strings, a unified recipient object, and a conversion object when the payout asset differs from the balance currency.

Follow these steps to transition to the new format. Each step is independent — you can adopt them incrementally.

New API keys automatically use the updated format. Go to the dashboard → API Keys → create a new key. Your existing key keeps working as-is — use the new key in a test environment first before switching production traffic.

Replace all session.* event type strings with the corresponding payment.* equivalents in your webhook handler.

payment.partial (previously session.partial) is now strictly mid-flow. It only fires when partial payments are enabled and the session is still open awaiting more funds. Terminal underpayment always uses payment.underpaid.

The session object has been restructured. Update any code that reads the nested amount object — amount and currency are now flat strings, status is replaced by state, and the financial breakdown moves to settlements.

Replace POST /payout/fiat and POST /payout/crypto with the unified POST /payouts. Add a method field to the request body and use recipientId instead of bankAccountId or addressId. Replace /payout/fiat/fee and /payout/crypto/fee with GET /payouts/fees.

In the old API, amount was the total debited from your balance (fees included). In the new API, amount is what the recipient receives — fees are charged separately. Remove any fee addition logic from your integration.

The response replaces the nested amount object and separate bankAccount/crypto objects with flat fields and a unified recipient object.

Open SearchKeyboard Shortcut:CTRL k Ask AI

Keyboard Shortcut:CTRL k

Keyboard Shortcut:

HTTP Method:  POST

HTTP Method:  GET

Retrieve invoice by reference

Crypto Payout Recipient

Fiat Payout Recipient

Powered by Scalar

Download OpenAPI Document

"https://mcp.coincircuit.io"

"2026-01-23T23:33:55.685Z"

"/api/v1/payments"

Server:https://api.coincircuit.io

https://api.coincircuit.io

Select Auth Type

No authentication selected

Client Libraries

More Select from all clients

Select from all clients

Invoices Operations

/api/v1/invoices

HeaderDescriptionX-CoinCircuit-EventThe event type, e.g. transaction.confirmedX-CoinCircuit-Delivery-IdUnique id for this deliveryX-CoinCircuit-SignatureHMAC-SHA256 signature of the payloadX-CoinCircuit-TimestampUnix timestamp of the delivery

GuidesIntroduction

NewIssue a deposit account per customer

POST /deposits/accounts

"123e4567-e89b-12d3-a456-426614174000"

// Omit "customerId" to create the parent (own-balance) account

NewIssue a pay-in identity into an account

// static crypto deposit address

POST /deposits/accounts/

"static_deposit_address"

// NGN virtual account

"ngn_virtual_account"

NewRetrieve and list deposit accounts

GET /deposits/accounts/

GET /deposits/accounts/by-customer/

GET /deposits/accounts?status=active&page=

NewGET /balance/transactions

GET /balance/transactions?source=payment_session¤cy=USDT

"Balance transactions retrieved successfully"

"bt_123456789_abcdef"

"payment_session"

"sourceReference"

"CC-1G3fDjgD93md"

"2026-06-01T10:30:00.000Z"

NewHow to locate the source of an entry

// session / invoice / payout entry

// swap entry (no reference)

// deposit entry (sourceId resolves at GET /deposits/:id)

"dep_123456789_abcdef"

Newdeposit.processing, deposit.completed and deposit.failed

"deposit.completed"

"depositAccountId"

"da_123456789_abcdef"

"customer@example.com"

"staticDepositAddress"

"2026-01-10T08:00:00.000Z"

"John Doe-RSRVD"

"ngnVirtualAccount"

"accountReference"

"CCVBA-7f3a9c12"

"2026-06-01T10:35:00.000Z"

Newswap.completed and swap.failed webhooks

"swap.completed"

"f2908051-6326-4708-bc73-1153209e7689"

"2026-05-29T20:25:23.680Z"

"2026-05-29T20:25:23.640Z"

"99f41fc8-47e2-4f34-a1dd-64f52ee4ff0c"

"2026-05-29T20:25:38.553Z"

"2026-05-29T20:25:23.556Z"

UpdatedDeposit webhooks renamed: balance_deposit.* → deposit.*

// Previous events "balance_deposit.processing" "balance_deposit.completed"

// Previous events

"balance_deposit.processing"

"balance_deposit.completed"

// New events "deposit.processing" "deposit.completed" "deposit.failed"

"deposit.processing"

"deposit.failed"

NewList and fetch deposits at /deposits

GET /balance/deposits/:id

GET /balance/deposits/

GET /deposits GET /deposits/:id

NewPOST /balance/swap/quotation

POST /balance/swap/quotation

"quotation-uuid"

"2026-05-12T10:00:15.000Z"

"2026-05-12T10:00:00.000Z"

NewPOST /balance/swap/execute/:quotationId

POST /balance/swap/execute/quotation-uuid

NewGET /balance/swap/estimate

GET /balance/swap/estimate?fromCurrency=USDT&toCurrency=NGN&amount=

NewGET /balance/swap/quotation/:quotationId

GET /balance/swap/quotation/quotation-uuid

NewPOST /payments/agent/settle

POST /api/v1/payments/agent/settle

"sessionReference"

"0xAgentWallet..."

"0xDepositAddress..."

"0xrandom32bytes..."

NewPOST /payments/agent/verify

POST /api/v1/payments/agent/verify

// Response (200)

Newx402 HTTP 402 payment flow support

example.javascript

resource delivered

NewThree gasless signing schemes supported

| Scheme | Assets | Chains |

NewBase blockchain support

NewArbitrum blockchain support

UpdatedWebhook event types renamed: session.* → payment.*

// Previous event types "session.completed" "session.expired" "session.partial" "session.failed"

// Previous event types

"session.completed"

"session.expired"

"session.partial"

"session.failed"

// New event types "payment.completed" "payment.expired" "payment.partial" "payment.underpaid" "payment.failed"

// New event types

"payment.completed"

"payment.expired"

"payment.partial"

"payment.underpaid"

"payment.failed"

NewNew API keys use the updated format automatically

// Existing key → receives current events and response shape

// New key → receives updated events and response shape

// No config needed — version is determined by which key you use

NewRedesigned session response shape

// New session response

"550e8400-e29b-41d4-a716-446655440000"

"cs_ref_abc123xyz789"

"amountReceived"

"conversionRate"

Newstate replaces status

// "pending" | "completed" | "expired" | "failed"

// "open" | "closed"

// payment outcome lives in payment.status

Newsettlements included in payment API response

UpdatedPayout API deprecated — migrate to /payouts by March 30

POST /payout/fiat // fiat payouts POST /payout/crypto // crypto payouts GET /payout/fiat/fee // fiat fee lookup GET /payout/crypto/fee // crypto fee lookup

POST /payout/fiat

POST /payout/crypto

// crypto payouts

GET /payout/fiat/fee

// fiat fee lookup

GET /payout/crypto/fee

// crypto fee lookup

POST /payouts // unified — specify method in body GET /payouts/fees // unified fee lookup

// unified — specify method in body

GET /payouts/fees

// unified fee lookup

NewUnified payout endpoint

// New unified request body

// "fiat" | "crypto"

// amount recipient receives

"7c9e6679-7425-40de-944b-e07fc1f90ae7"

"CLIENT-REF-123"

// optional idempotency key

UpdatedAmount is what the recipient receives

// Old: amount = total deducted (fee included)

// recipient gets: 10000.00

// New: amount = what recipient gets (fee is separate)

// total deducted: 10050.00

NewRedesigned payout response

// New payout response (USDT balance → USDC payout)

"convertedAmount"

// Replace each event type:

// payment.partial → session still open, more payments expected

// data.session.state === "open"

notifyCustomerOfPartialPayment

// payment.underpaid → session closed, insufficient total received

// data.session.state === "closed"

initiateRefundFlow

// Previous — nested amount object

// New — flat amount + settlements object

// "10000.00" (plain string)

// "NGN" (plain string)

// Old — separate endpoints

// New — unified endpoint

// was balanceCurrency

// was bankAccountId or addressId

// optional idempotency key (new)

// Old — amount includes fee, recipient gets less

// you had to add fee

// recipient gets 10000.00, fee was 50.00

// New — amount IS what recipient gets

// recipient gets 10000.00, fee (50.00) charged separately

// total deducted from balance: 10050.00

// Old response fields

// nested object

// New response fields

// plain string (what recipient gets)

// plain string (amount + fee)

// "bankAccount" | "cryptoAddress"

// cross-currency (e.g. USDT→USDC) or null

  • InvoicesClose Group Create invoiceHTTP Method:  POSTList invoicesHTTP Method:  GETRetrieve invoice by referenceHTTP Method:  GET
  • Create invoiceHTTP Method:  POST
  • List invoicesHTTP Method:  GET
  • Retrieve invoice by referenceHTTP Method:  GET
  • PaymentsOpen Group
  • TransactionsOpen Group
  • DepositsOpen Group
  • PayoutsOpen Group
  • Crypto Payout RecipientOpen Group
  • Fiat Payout RecipientOpen Group
  • BalanceOpen Group
  • SwapOpen Group
  • SettlementsOpen Group
  • CustomersOpen Group
  • BlockchainOpen Group
  • RatesOpen Group
  • RefundsOpen Group
  • Payment PagesOpen Group
  • ModelsOpen Group
  • Production Base URL: https://api.coincircuit.io
  • Sandbox Base URL: https://sandbox-api.coincircuit.io
  • post/api/v1/invoices
  • get/api/v1/invoices
  • get/api/v1/invoices/reference/{reference}
  • Accept crypto payments online and in person
  • Send payouts to bank accounts and crypto wallets
  • Issue refunds and reconcile settlements
  • Track every state change with real-time webhooks
  • Production: https://api.coincircuit.io
  • Sandbox: https://sandbox-api.coincircuit.io
  • v2.3.0June 2026
  • v2.2.0May 2026
  • v2.1.0March 2026
  • v2.0.0February 2026
  • Quick Start PyPI Webhooks tab

    HTTP Status

    Description

    400

    Invalid input or malformed request

    401

    Invalid or missing authentication

    404

    Resource not found

    409

    Resource conflict

    503

    Service unavailable

    Header

    X-CoinCircuit-Event

    The event type, e.g. transaction.confirmed

    X-CoinCircuit-Delivery-Id

    Unique id for this delivery

    X-CoinCircuit-Signature

    HMAC-SHA256 signature of the payload

    X-CoinCircuit-Timestamp

    Unix timestamp of the delivery

    https://sandbox-api.coincircuit.io
    https://mcp.coincircuit.io
    {
      "mcpServers": {
        "coincircuit": {
          "url": "https://mcp.coincircuit.io",
          "headers": {
            "x-mcp-key": "YOUR_MCP_KEY"
          }
        }
      }
    }
    npm install coincircuit
    pip install coincircuit
    npm install @coincircuit/checkout
    x-api-key: your_api_key_here
    {
        "success": false,
        "timestamp": "2026-01-23T23:33:55.685Z",
        "path": "/api/v1/payments",
        "method": "POST",
        "error": "Unauthorized",
        "message": "Unauthorized"
    }
    429 Too Many Requests
    "meta": {
      "page": 1,
      "size": 10,
      "total": 42,
      "totalPages": 5
    }
    transaction.confirmed
    POST /deposits/accounts
    { "customerId": "123e4567-e89b-12d3-a456-426614174000" }
    
    // Omit "customerId" to create the parent (own-balance) account
    // static crypto deposit address
    POST /deposits/accounts/:id/identities
    { "type": "static_deposit_address", "chain": "ethereum" }
    
    // NGN virtual account
    POST /deposits/accounts/:id/identities
    { "type": "ngn_virtual_account", "bvn": "12345678901" }
    GET /deposits/accounts/:id
    GET /deposits/accounts/parent
    GET /deposits/accounts/by-customer/:customerId
    GET /deposits/accounts?status=active&page=1&size=20
    GET /balance/transactions?source=payment_session¤cy=USDT
    
    {
      "success": true,
      "message": "Balance transactions retrieved successfully",
      "data": [
        {
          "id": "bt_123456789_abcdef",
          "source": "payment_session",
          "sourceId": "123e4567-e89b-12d3-a456-426614174000",
          "sourceReference": "CC-1G3fDjgD93md",
          "direction": "credit",
          "status": "completed",
          "amount": "100.00",
          "currency": "USDT",
          "fee": "0",
          "reference": "...",
          "bala
    // session / invoice / payout entry
    { "source": "invoice", "sourceId": "...", "sourceReference": "CC-INV-001" }
    
    // swap entry (no reference)
    { "source": "swap", "sourceId": "...", "sourceReference": null }
    
    // deposit entry (sourceId resolves at GET /deposits/:id)
    { "source": "deposit", "sourceId": "dep_123456789_abcdef", "sourceReference": null }
    {
      "event": "deposit.completed",
      "data": {
        "id": "dep_123456789_abcdef",
        "type": "crypto",
        "status": "completed",
        "amount": "100.00",
        "currency": "USDT",
        "fee": "0.50",
        "netAmount": "99.50",
        "depositAccountId": "da_123456789_abcdef",
        "customer": {
          "id": "123e4567-e89b-12d3-a456-426614174000",
          "firstName": "John",
          "lastName": "Doe",
          "email": "customer@example.com"
        },
        "crypto": {
          "txHash": "0x...",
          "chain": "ethereu
    {
      "event": "swap.completed",
      "data": {
        "swap": {
          "id": "f2908051-6326-4708-bc73-1153209e7689",
          "fromCurrency": "USDT",
          "toCurrency": "NGN",
          "sourceAmount": "4.00",
          "targetAmount": "5493.95",
          "rate": "1373.48767500",
          "status": "completed",
          "completedAt": "2026-05-29T20:25:23.680Z",
          "createdAt": "2026-05-29T20:25:23.640Z",
          "quotation": {
            "id": "99f41fc8-47e2-4f34-a1dd-64f52ee4ff0c",
            "fromCurrency": "USDT",
            
    POST /balance/swap/quotation
    {
      "fromCurrency": "USDT",
      "toCurrency": "NGN",
      "amount": "500.00"
    }
    
    // Response
    {
      "id": "quotation-uuid",
      "fromCurrency": "USDT",
      "toCurrency": "NGN",
      "sourceAmount": "500.00",
      "targetAmount": "747500.00",
      "rate": "1495.00000000",
      "executed": false,
      "expiresAt": "2026-05-12T10:00:15.000Z",
      "createdAt": "2026-05-12T10:00:00.000Z"
    }
    POST /balance/swap/execute/quotation-uuid
    
    // Response
    {
      "success": true,
      "swapId": "swap-uuid",
      "fromCurrency": "USDT",
      "toCurrency": "NGN",
      "sourceAmount": "500.00",
      "targetAmount": "747500.00",
      "rate": "1495.00000000"
    }
    GET /balance/swap/estimate?fromCurrency=USDT&toCurrency=NGN&amount=500.00
    
    // Response
    {
      "fromCurrency": "USDT",
      "toCurrency": "NGN",
      "sourceAmount": "500.00",
      "targetAmount": "747500.00",
      "rate": "1495.00000000"
    }
    GET /balance/swap/quotation/quotation-uuid
    
    // Response
    {
      "id": "quotation-uuid",
      "fromCurrency": "USDT",
      "toCurrency": "NGN",
      "sourceAmount": "500.00",
      "targetAmount": "747500.00",
      "rate": "1495.00000000",
      "executed": true,
      "expiresAt": "2026-05-12T10:00:15.000Z",
      "createdAt": "2026-05-12T10:00:00.000Z"
    }
    POST /api/v1/payments/agent/settle
    {
      "sessionReference": "cs_ref_abc123",
      "scheme": "eip3009",
      "chain": "base",
      "asset": "USDC",
      "payload": {
        "from": "0xAgentWallet...",
        "to": "0xDepositAddress...",
        "value": "1000000",
        "validAfter": "0",
        "validBefore": "1774055094",
        "nonce": "0xrandom32bytes...",
        "signature": "0xabcd...1234"
      }
    }
    POST /api/v1/payments/agent/verify
    {
      "scheme": "eip3009",
      "chain": "base",
      "asset": "USDC",
      "payload": { ... }
    }
    
    // Response (200)
    {
      "valid": true,
      "checks": {
        "balance": "pass",
        "nonce": "pass",
        "authorization": "pass"
      }
    }
    // x402 Flow
    Agent -> GET /api/premium-data
    Server -> CoinCircuit: POST /payments (creates session)
    Server -> Agent: 402 (deposit address, amount)
    Agent -> Signs payment (eip3009 / permit2 / solana)
    Agent -> Server: POST /pay (signed payload)
    Server -> CoinCircuit: POST /payments/agent/settle
    Server -> Agent: 200 (resource delivered)
    | Scheme  | Assets       | Chains              |
    | eip3009 | USDC         | Base, Arbitrum      |
    | permit2 | USDC, USDT   | Base, Arbitrum, BSC |
    | solana  | SOL,USDC,USDT| Solana              |
    // Existing key → receives current events and response shape
    // New key → receives updated events and response shape
    
    // No config needed — version is determined by which key you use
    // New session response
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "reference": "cs_ref_abc123xyz789",
      "state": "closed",
      "type": "payment",
      "amount": "10000.00",
      "currency": "NGN",
      "payment": {
        "status": "completed",
        "asset": "USDT",
        "chain": "tron",
        "amount": "6.84",
        "amountReceived": "6.84",
        "address": "T...",
        "txHash": "0x..."
      },
      "settlements": {
        "currency": "NGN",
        "gross": { "amount": "10000.00", "conversionRate": "1.0" },
        "fees
    // Previous
    { "status": "completed" }  // "pending" | "completed" | "expired" | "failed"
    
    // New
    { "state": "closed" }     // "open" | "closed"
    // payment outcome lives in payment.status
    {
      "settlements": {
        "currency": "NGN",
        "gross": { "amount": "10000.00", "conversionRate": "1.0" },
        "fees": {
          "processing": { "amount": "100.00", "paidBy": "merchant" },
          "gas": { "amount": "50.00", "paidBy": "merchant" }
        },
        "net": { "amount": "9850.00" }
      }
    }
    // New unified request body
    {
      "method": "fiat",       // "fiat" | "crypto"
      "currency": "NGN",
      "amount": "10000.00",   // amount recipient receives
      "recipientId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "narration": "Payout",
      "reference": "CLIENT-REF-123" // optional idempotency key
    }
    // Old: amount = total deducted (fee included)
    { "amount": "10050.00" }
    // fee: 50.00
    // recipient gets: 10000.00
    // New: amount = what recipient gets (fee is separate)
    { "amount": "10000.00" }
    // fee: 50.00
    // total deducted: 10050.00
    // New payout response (USDT balance → USDC payout)
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "method": "crypto",
      "type": "manual",
      "status": "success",
      "amount": "100.00",
      "fee": "1.50",
      "total": "101.50",
      "currency": "USDC",
      "conversion": {
        "from": "USDT",
        "to": "USDC",
        "rate": "0.9998",
        "convertedAmount": "100.00"
      },
      "recipient": {
        "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "type": "cryptoAddress",
        "address": "T...",
        "chain": "tro
    // Replace each event type:
    "session.completed" → "payment.completed"
    "session.expired"   → "payment.expired"
    "session.partial"   → "payment.partial"
    "session.failed"    → "payment.failed"
    // payment.partial → session still open, more payments expected
    if (event === "payment.partial") {
      // data.session.state === "open"
      notifyCustomerOfPartialPayment(data.session.payment.amountReceived);
    }
    
    // payment.underpaid → session closed, insufficient total received
    if (event === "payment.underpaid") {
      // data.session.state === "closed"
      initiateRefundFlow(data.session.payment.amountReceived);
    }
    // Previous — nested amount object
    session.status                  // "pending" | "completed" | "expired" | "failed"
    session.amount.requested.amount // "10000.00"
    session.amount.requested.currency // "NGN"
    session.amount.gross            // { amount, conversionRate }
    session.amount.fees             // { processing, gas }
    session.amount.net              // { amount }
    
    // New — flat amount + settlements object
    session.state                   // "open" | "closed"
    session.amount                  // 
    // Old — separate endpoints
    POST /payout/fiat   { balanceCurrency, amount, bankAccountId, narration }
    POST /payout/crypto { balanceCurrency, amount, addressId }
    
    // New — unified endpoint
    POST /payouts {
      method: "fiat" | "crypto",
      currency: "NGN",          // was balanceCurrency
      amount: "10000.00",
      recipientId: "...",        // was bankAccountId or addressId
      narration: "...",
      reference: "..."           // optional idempotency key (new)
    }
    // Old — amount includes fee, recipient gets less
    const amount = recipientAmount + fee; // you had to add fee
    POST /payout/fiat { amount: "10050.00" }
    // recipient gets 10000.00, fee was 50.00
    
    // New — amount IS what recipient gets
    POST /payouts { amount: "10000.00" }
    // recipient gets 10000.00, fee (50.00) charged separately
    // total deducted from balance: 10050.00
    // Old response fields
    payout.amount.gross         // nested object
    payout.amount.fee
    payout.amount.net
    payout.amount.currency
    payout.bankAccount          // fiat only
    payout.crypto.amount        // crypto only
    payout.crypto.chain
    payout.crypto.address
    
    // New response fields
    payout.amount               // plain string (what recipient gets)
    payout.fee                  // plain string
    payout.total                // plain string (amount + fee)
    payout.currency             // plain string
    payout.rec