# Get Balance Source: https://docs.unwall.xyz/api-reference/agent/get-balance GET /v1/balance Get the current USDC wallet balance. Returns the current USDC balance, spending totals, and wallet address for the project linked to the authenticated API token. All monetary values are in micro-USDC (1 USDC = 1,000,000). Requires a bearer token with the `read` permission. ## Request This endpoint takes no query parameters or request body. ## Response Unique project identifier. Always `"usdc"`. Available USDC balance in micro-USDC. Pending USDC balance in micro-USDC. Funds in transit that are not yet available. Cumulative USDC funded into this project in micro-USDC. Cumulative USDC spent from this project in micro-USDC. Bridge wallet address on Base for receiving USDC deposits. `null` if no wallet has been created yet. ## Examples ```bash curl theme={null} curl https://api.unwall.xyz/v1/balance \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```python Python theme={null} import requests resp = requests.get( "https://api.unwall.xyz/v1/balance", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, ) balance = resp.json() usdc = balance["available"] / 1_000_000 print(f"Balance: {usdc:.2f} USDC") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/balance", { headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", }, }); const balance = await resp.json(); console.log(`Balance: ${(balance.available / 1_000_000).toFixed(2)} USDC`); ``` ```json Response (200 OK) theme={null} { "project_id": "proj_abc123", "currency": "usdc", "available": 5000000, "pending": 0, "total_funded": 5000000, "total_spent": 0, "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28" } ``` # Get Deposit Address Source: https://docs.unwall.xyz/api-reference/agent/get-deposit-address GET /v1/stablecoin/address Get the on-chain USDC deposit address for your project. A Bridge wallet is auto-created if one doesn't exist. Returns the project's USDC deposit address on Base. Send USDC to this address to fund the project wallet. The balance updates automatically when the deposit is confirmed on-chain via Bridge.xyz webhook. If the project does not yet have a Bridge.xyz wallet, one is created automatically on first call. Requires a bearer token with the `read` permission. ## Request This endpoint takes no query parameters or request body. ## Response Unique project identifier. Blockchain network. Always `"base"`. EVM wallet address for receiving USDC deposits. Deposit currency. Always `"usdc"`. ## Examples ```bash curl theme={null} curl https://api.unwall.xyz/v1/stablecoin/address \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```python Python theme={null} import requests resp = requests.get( "https://api.unwall.xyz/v1/stablecoin/address", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, ) data = resp.json() print(f"Deposit USDC on {data['chain']} to: {data['address']}") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/stablecoin/address", { headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", }, }); const data = await resp.json(); console.log(`Deposit USDC on ${data.chain} to: ${data.address}`); ``` ```json Response (200 OK) theme={null} { "project_id": "proj_abc123", "chain": "base", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "currency": "usdc" } ``` Only send USDC on the Base network to this address. Sending other tokens or using the wrong network may result in permanently lost funds. # List Transactions Source: https://docs.unwall.xyz/api-reference/agent/list-transactions GET /v1/transactions Retrieve a paginated list of transactions for the project associated with the API token. Returns a paginated list of all transactions for the project linked to the authenticated API token. Results are ordered by creation time, most recent first. Requires a bearer token with the `read` permission. ## Query Parameters Number of transactions to return. Min 1, max 200. Number of transactions to skip. Min 0. ## Response Array of transaction objects. Unique transaction identifier. Transaction type. One of: `fund`, `payment`, `x402_payment`, `usdc_transfer`, `usdc_fund`, `platform_fee`, `fiat_withdrawal`. Transaction status: `pending`, `processing`, `completed`, `failed`, or `reversed`. Transaction amount in currency units (cents for USD, micro-USDC for USDC). Currency code: `usd` or `usdc`. Human-readable description of the transaction. Merchant or recipient name, if applicable. ISO 8601 timestamp of when the transaction was created. ISO 8601 timestamp of when the transaction completed. `null` if still in progress. Total number of transactions across all pages. Whether there are more transactions beyond the current page. ## Transaction Types | Type | Description | | ----------------- | ------------------------------------------------ | | `fund` | Fiat funding added to the project | | `payment` | Outbound ACH payment to an external bank account | | `x402_payment` | x402 protocol payment to a URL | | `usdc_transfer` | On-chain USDC transfer to an external wallet | | `usdc_fund` | USDC deposit received on-chain | | `platform_fee` | Platform fee charged on a transaction | | `fiat_withdrawal` | Fiat withdrawal from the project | ## Examples ```bash curl theme={null} curl "https://api.unwall.xyz/v1/transactions?limit=10&offset=0" \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```python Python theme={null} import requests resp = requests.get( "https://api.unwall.xyz/v1/transactions", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, params={"limit": 10, "offset": 0}, ) data = resp.json() for tx in data["transactions"]: print(f"{tx['type']} — {tx['amount']} {tx['currency']} — {tx['status']}") print(f"Showing {len(data['transactions'])} of {data['total_count']}") ``` ```typescript TypeScript theme={null} const resp = await fetch( "https://api.unwall.xyz/v1/transactions?limit=10&offset=0", { headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", }, } ); const data = await resp.json(); for (const tx of data.transactions) { console.log(`${tx.type} — ${tx.amount} ${tx.currency} — ${tx.status}`); } console.log(`Showing ${data.transactions.length} of ${data.total_count}`); ``` ```json Response (200 OK) theme={null} { "transactions": [ { "id": "tx_x402_001", "type": "x402_payment", "status": "completed", "amount": 500000, "currency": "usdc", "description": "x402 payment to https://api.example.com/v1/data", "merchant_name": "api.example.com", "created_at": "2026-03-11T16:30:00Z", "completed_at": "2026-03-11T16:30:05Z" }, { "id": "tx_usdc_002", "type": "usdc_transfer", "status": "processing", "amount": 50000000, "currency": "usdc", "description": "Vendor payment", "merchant_name": null, "created_at": "2026-03-11T14:00:00Z", "completed_at": null }, { "id": "tx_fund_003", "type": "usdc_fund", "status": "completed", "amount": 100000000, "currency": "usdc", "description": "USDC deposit", "merchant_name": null, "created_at": "2026-03-10T09:00:00Z", "completed_at": "2026-03-10T09:00:30Z" } ], "total_count": 3, "has_more": false } ``` # Send Payment (Legacy) Source: https://docs.unwall.xyz/api-reference/agent/send-payment POST /v1/payments Send an outbound fiat payment to an external bank account via ACH. This endpoint is deprecated. Use [POST /v1/pay](/api-reference/agent/unified-pay) with a bank details recipient object instead. Initiates an outbound fiat payment from the project's wallet to an external US bank account via Bridge.xyz off-ramp. The project balance is debited atomically before the transfer is initiated. If the transfer fails, the balance is automatically restored. Requires a bearer token with the `pay` permission. ## Request Body Payment amount in cents. Must be between 1 and 100,000,000 (\$1,000,000.00). Recipient bank account details. Recipient name. 1-200 characters. Bank account number. 4-34 characters. ABA routing number. Exactly 9 digits. Recipient email address. Up to 254 characters. Payment description for record-keeping. Max 500 characters. Unique key to prevent duplicate payments. Alphanumeric plus `_`, `-`, `:`, `.`. Max 255 characters. ## Response Unique transaction identifier. Transaction status: `pending`, `processing`, `completed`, or `failed`. Payment amount in cents. Name of the payment recipient. ISO 8601 timestamp of when the payment was created. Estimated delivery time (e.g., "2-3 business days"). `null` if no ACH transfer was initiated. ## Examples ```bash curl theme={null} curl -X POST https://api.unwall.xyz/v1/payments \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "amount": 250000, "recipient": { "name": "Acme Supplies Inc", "account_number": "9876543210", "routing_number": "021000021", "email": "billing@acme.com" }, "description": "Monthly supply order #47", "idempotency_key": "order-47-2026-03" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.unwall.xyz/v1/payments", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, json={ "amount": 250000, "recipient": { "name": "Acme Supplies Inc", "account_number": "9876543210", "routing_number": "021000021", "email": "billing@acme.com", }, "description": "Monthly supply order #47", "idempotency_key": "order-47-2026-03", }, ) result = resp.json() print(f"Payment {result['id']} — {result['status']}") print(f"Arrives: {result['estimated_arrival']}") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/payments", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ amount: 250000, recipient: { name: "Acme Supplies Inc", account_number: "9876543210", routing_number: "021000021", email: "billing@acme.com", }, description: "Monthly supply order #47", idempotency_key: "order-47-2026-03", }), }); const result = await resp.json(); console.log(`Payment ${result.id} — ${result.status}`); console.log(`Arrives: ${result.estimated_arrival}`); ``` ```json Response (201 Created) theme={null} { "id": "tx_fiat_abc123", "status": "processing", "amount": 250000, "recipient_name": "Acme Supplies Inc", "created_at": "2026-03-11T14:30:00Z", "estimated_arrival": "2-3 business days" } ``` ## Idempotency If you provide an `idempotency_key` and a transaction with that key already exists for this project, the API returns the original transaction with a `200 OK` status (not `201 Created`). The request body is not re-evaluated. Always include an idempotency key when sending payments from automated agents. Use a deterministic key tied to your business logic (e.g., invoice number, order ID) to guarantee at-most-once delivery. # Send Payment Source: https://docs.unwall.xyz/api-reference/agent/unified-pay POST /v1/pay Unified payment endpoint -- auto-routes to x402, USDC transfer, or fiat ACH based on the recipient. The unified pay endpoint is the recommended way to send any payment through Unwall. It automatically detects the recipient type and routes to the appropriate rail: * **URL** (e.g., `https://api.example.com/...`) -- routes to the x402 protocol * **EVM address** (e.g., `0x742d...`) -- routes to on-chain USDC transfer * **Bank details object** -- routes to fiat ACH off-ramp Requires a bearer token with the `pay` permission. If the recipient is a URL, the token must also have the `x402` permission. ## Request Body The payment recipient. Accepts one of three formats: * **URL** (string): An HTTPS URL for x402 protocol payments (e.g., `"https://api.example.com/v1/data"`) * **EVM address** (string): A `0x`-prefixed Ethereum address for USDC transfers (e.g., `"0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28"`) * **Bank details** (object): An object with `name`, `account_number`, `routing_number`, and `email` for fiat ACH payments Recipient name. 1-200 characters. Bank account number. 4-34 characters. ABA routing number. Exactly 9 digits. Recipient email address. Up to 254 characters. Payment amount in USD. Required for USDC transfer and fiat rails. Not used for x402 (the target API sets the price). Maximum micro-USDC to pay for x402 requests (safety cap). 1 USDC = 1,000,000 micro-USDC. Only used when recipient is a URL. Payment description for record-keeping. Max 500 characters. Unique key to prevent duplicate payments. Alphanumeric plus `_`, `-`, `:`, `.`. Max 255 characters. HTTP method for x402 requests. One of `GET`, `POST`, `PUT`, `DELETE`. Only used when recipient is a URL. Additional HTTP headers for x402 requests. Only used when recipient is a URL. Request body for x402 `POST` or `PUT` requests. Only used when recipient is a URL. ## Response Unique transaction identifier. Transaction status: `pending`, `processing`, `completed`, or `failed`. Payment rail used: `x402`, `usdc_transfer`, or `fiat`. Amount charged in rail-native units. Micro-USDC for `x402` and `usdc_transfer` rails, cents for `fiat` rail. Platform fee charged in rail-native units. Currency of the amount: `usdc` or `usd`. Recipient identifier (URL, EVM address, or recipient name). On-chain transaction hash for USDC and x402 payments. `null` for fiat payments. ISO 8601 timestamp of when the payment was created. Estimated delivery time for fiat payments (e.g., "2-3 business days"). `null` for crypto rails. For x402 payments only -- contains the proxied API response. `null` for other rails. HTTP status code from the target API. Response headers from the target API. Response body from the target API. ## Examples ### x402 Payment (URL recipient) ```bash curl theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": "https://api.example.com/v1/data", "max_amount_usdc": 1000000, "description": "Fetch market data" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.unwall.xyz/v1/pay", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, json={ "recipient": "https://api.example.com/v1/data", "max_amount_usdc": 1000000, "description": "Fetch market data", }, ) result = resp.json() print(f"Paid {result['amount_charged'] / 1_000_000} USDC via {result['rail']}") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: "https://api.example.com/v1/data", max_amount_usdc: 1000000, description: "Fetch market data", }), }); const result = await resp.json(); console.log(`Paid ${result.amount_charged / 1_000_000} USDC via ${result.rail}`); ``` ```json Response (200 OK) theme={null} { "id": "tx_x402_abc123", "status": "completed", "rail": "x402", "amount_charged": 500000, "fee": 7500, "currency": "usdc", "recipient": "https://api.example.com/v1/data", "tx_hash": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890", "created_at": "2026-03-11T14:30:00Z", "estimated_arrival": null, "response": { "status_code": 200, "headers": { "content-type": "application/json" }, "body": "{\"data\": [{\"id\": 1, \"value\": \"premium result\"}]}" } } ``` ### USDC Transfer (EVM address recipient) ```bash curl theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "amount_usd": 50.00, "description": "Vendor payment" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.unwall.xyz/v1/pay", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, json={ "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "amount_usd": 50.00, "description": "Vendor payment", }, ) result = resp.json() print(f"Sent {result['amount_charged'] / 1_000_000} USDC to {result['recipient']}") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", amount_usd: 50.0, description: "Vendor payment", }), }); const result = await resp.json(); console.log(`Sent ${result.amount_charged / 1_000_000} USDC to ${result.recipient}`); ``` ```json Response (201 Created) theme={null} { "id": "tx_usdc_def456", "status": "processing", "rail": "usdc_transfer", "amount_charged": 50000000, "fee": 750000, "currency": "usdc", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "created_at": "2026-03-11T14:35:00Z", "estimated_arrival": null, "response": null } ``` ### Fiat ACH Payment (Bank details recipient) ```bash curl theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": { "name": "Acme Supplies Inc", "account_number": "9876543210", "routing_number": "021000021", "email": "billing@acme.com" }, "amount_usd": 2500.00, "description": "Monthly supply order #47" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.unwall.xyz/v1/pay", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, json={ "recipient": { "name": "Acme Supplies Inc", "account_number": "9876543210", "routing_number": "021000021", "email": "billing@acme.com", }, "amount_usd": 2500.00, "description": "Monthly supply order #47", }, ) result = resp.json() print(f"Payment {result['id']} — arrives {result['estimated_arrival']}") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: { name: "Acme Supplies Inc", account_number: "9876543210", routing_number: "021000021", email: "billing@acme.com", }, amount_usd: 2500.0, description: "Monthly supply order #47", }), }); const result = await resp.json(); console.log(`Payment ${result.id} — arrives ${result.estimated_arrival}`); ``` ```json Response (201 Created) theme={null} { "id": "tx_fiat_ghi789", "status": "processing", "rail": "fiat", "amount_charged": 250000, "fee": 3750, "currency": "usd", "recipient": "Acme Supplies Inc", "tx_hash": null, "created_at": "2026-03-11T14:40:00Z", "estimated_arrival": "2-3 business days", "response": null } ``` # Send USDC Source: https://docs.unwall.xyz/api-reference/agent/usdc-transfer POST /v1/usdc/transfer Send USDC on-chain to an external wallet address. This endpoint is deprecated. Use [POST /v1/pay](/api-reference/agent/unified-pay) with an EVM address recipient instead. Sends USDC on-chain from the project's Bridge.xyz wallet to an external wallet address on Base. Requires a bearer token with the `pay` permission. ## Request Body Amount of USDC to send (e.g., `10.50` for 10.50 USDC). Max 100,000. Destination EVM address. Must be `0x` followed by 40 hexadecimal characters. Cannot be the zero address. Blockchain network to send on. Default: `"base"`. Payment description for record-keeping. Max 500 characters. Unique key to prevent duplicate transfers. Max 255 characters. ## Response Unique transaction identifier. Transaction status: `pending`, `processing`, `completed`, or `failed`. Amount of USDC sent. Platform fee in USDC. Destination wallet address. Blockchain network used. On-chain transaction hash. `null` while the transaction is pending. ISO 8601 timestamp of when the transfer was created. ## Examples ```bash curl theme={null} curl -X POST https://api.unwall.xyz/v1/usdc/transfer \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "amount_usdc": 25.00, "to_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "description": "Contractor payment", "idempotency_key": "pay-contractor-2026-03" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.unwall.xyz/v1/usdc/transfer", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, json={ "amount_usdc": 25.00, "to_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "description": "Contractor payment", "idempotency_key": "pay-contractor-2026-03", }, ) result = resp.json() print(f"Sent {result['amount_usdc']} USDC — tx: {result['tx_hash']}") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/usdc/transfer", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ amount_usdc: 25.0, to_address: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", description: "Contractor payment", idempotency_key: "pay-contractor-2026-03", }), }); const result = await resp.json(); console.log(`Sent ${result.amount_usdc} USDC — tx: ${result.tx_hash}`); ``` ```json Response (201 Created) theme={null} { "id": "tx_usdc_abc123", "status": "processing", "amount_usdc": 25.0, "fee_usdc": 0.375, "to_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "chain": "base", "tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "created_at": "2026-03-11T14:30:00Z" } ``` # x402 Pay Source: https://docs.unwall.xyz/api-reference/agent/x402-pay POST /v1/x402/pay Proxy an API call through the x402 payment protocol, automatically paying in USDC. This endpoint is deprecated. Use [POST /v1/pay](/api-reference/agent/unified-pay) with a URL recipient instead. Proxies an HTTP request to a target API. If the API returns `HTTP 402 Payment Required`, the backend automatically signs an EIP-3009 USDC authorization, retries the request with the x402 payment header, and returns the API response. Requires a bearer token with the `x402` permission. ## Request Body Target API URL. Max 2048 characters. HTTP method for the request. One of `GET`, `POST`, `PUT`, `DELETE`. Additional HTTP headers to include in the request to the target API. Request body for `POST` or `PUT` requests. Maximum micro-USDC to pay (safety cap). 1 USDC = 1,000,000 micro-USDC. Must be greater than 0 and at most 100,000,000 (100 USDC). ## Response HTTP status code returned by the target API. Response headers from the target API. Response body from the target API. Actual micro-USDC charged. `0` if the target API did not require payment. Platform fee in micro-USDC. On-chain settlement transaction hash. `null` if no payment was required. Whether the payment was settled on-chain. ## How It Works 1. The backend makes the initial request to the target URL. 2. If the API returns `HTTP 402 Payment Required`, the response body contains the x402 payment requirements. 3. The backend signs an EIP-3009 USDC authorization using the project's x402 wallet key. 4. The request is retried with an `X-PAYMENT` header containing the signed payment. 5. The x402 facilitator verifies and settles the payment on-chain. 6. The API response is returned to the agent. 7. The USDC amount and platform fee are recorded in the project's ledger. If the target API does not return 402 (returns 200 directly), the response is passed through without any payment. ## Examples ```bash curl theme={null} curl -X POST https://api.unwall.xyz/v1/x402/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.example.com/v1/premium-data", "method": "GET", "max_amount_usdc": 500000 }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.unwall.xyz/v1/x402/pay", headers={"Authorization": "Bearer aw_live_xxxxxxxxxxxx"}, json={ "url": "https://api.example.com/v1/premium-data", "method": "GET", "max_amount_usdc": 500000, }, ) result = resp.json() print(f"Status: {result['status_code']}") print(f"Paid: {result['payment_amount'] / 1_000_000} USDC") print(f"Body: {result['body']}") ``` ```typescript TypeScript theme={null} const resp = await fetch("https://api.unwall.xyz/v1/x402/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://api.example.com/v1/premium-data", method: "GET", max_amount_usdc: 500000, }), }); const result = await resp.json(); console.log(`Status: ${result.status_code}`); console.log(`Paid: ${result.payment_amount / 1_000_000} USDC`); console.log(`Body: ${result.body}`); ``` ```json Response (200 OK) theme={null} { "status_code": 200, "headers": { "content-type": "application/json" }, "body": "{\"data\": [{\"id\": 1, \"value\": \"premium result\"}]}", "payment_amount": 100000, "fee_amount": 1500, "tx_hash": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890", "settled": true } ``` ## Errors | Status | Cause | | ------ | ----------------------------------------------------------- | | `400` | `max_amount_usdc` exceeded by the API's payment requirement | | `400` | Insufficient USDC balance | | `403` | Token lacks `x402` permission | | `502` | Target API or x402 facilitator returned an error | The `max_amount_usdc` is a safety cap. If the target API requests more USDC than your max, the payment is rejected and the request fails with a 400 error. Always set a reasonable max for the API you are calling. # API Overview Source: https://docs.unwall.xyz/api-reference/overview Base URL, authentication, error handling, and conventions for the Unwall API. ## Base URL All API requests are made to: ``` https://api.unwall.xyz ``` ## Authentication Authenticate every request by including your API token in the `Authorization` header: ``` Authorization: Bearer aw_live_xxxxxxxxxxxx ``` Tokens are scoped with granular permissions: | Permission | Description | | ---------- | -------------------------------------------------- | | `read` | Read balances, transactions, and deposit addresses | | `pay` | Send payments via USDC, fiat, or unified pay | | `x402` | Make x402 protocol payments to URLs | Keep your API tokens secret. Do not expose them in client-side code or public repositories. If a token is compromised, rotate it immediately from the dashboard. ## Content Type All requests and responses use JSON: ``` Content-Type: application/json ``` ## Monetary Values Unwall uses integer representations for all monetary values to avoid floating-point precision issues. | Currency | Unit | Example | | -------- | ---------- | --------------------- | | USD | Cents | `5000` = \$50.00 | | USDC | Micro-USDC | `1000000` = 1.00 USDC | ## Pagination List endpoints support pagination with `limit` and `offset` parameters. Number of results to return. Min 1, max 200. Number of results to skip before returning. Paginated responses include: Whether there are more results beyond the current page. Total number of matching results. ## Idempotency To safely retry requests without duplicating side effects, include an `idempotency_key` in the request body of any write operation. A unique key to prevent duplicate operations. Alphanumeric characters plus `_`, `-`, `:`, and `.` are allowed. Max 255 characters. If a request with the same idempotency key has already been processed, the original response is returned. Always include an idempotency key when sending payments from automated systems. Use a deterministic key derived from your application state (e.g., invoice number, order ID). ## Rate Limits API requests are rate-limited to **100 requests per minute** per token. When the rate limit is exceeded, the API returns HTTP `429 Too Many Requests` with a `Retry-After` header indicating how many seconds to wait before retrying. ``` HTTP/1.1 429 Too Many Requests Retry-After: 12 ``` ## Error Format All errors return a JSON object with a `detail` field containing a human-readable error message: ```json theme={null} { "detail": "Human-readable error message" } ``` ## Status Codes | Code | Description | | ----- | ------------------------------------------------------------ | | `200` | OK -- Request succeeded | | `201` | Created -- Resource created successfully | | `400` | Bad Request -- Invalid parameters or missing required fields | | `401` | Unauthorized -- Missing or invalid API token | | `403` | Forbidden -- Token lacks the required permission | | `404` | Not Found -- Resource does not exist | | `429` | Too Many Requests -- Rate limit exceeded | | `502` | Bad Gateway -- Upstream service error (retry with backoff) | # Authentication Source: https://docs.unwall.xyz/authentication Secure your API calls with scoped tokens and understand rate limits. ## Bearer Token All Unwall API requests require a bearer token in the `Authorization` header. Tokens are project-scoped and prefixed with `aw_live_`. ```bash theme={null} curl https://api.unwall.xyz/v1/balance \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` Tokens are SHA-256 hashed before storage -- the plaintext value is never saved on our servers. Each token belongs to exactly one project and can be revoked instantly from the dashboard. ## Permissions Tokens carry independent permission scopes. Only grant the permissions your agent actually needs. | Permission | Grants Access To | | ---------- | --------------------------------------------------------------------------------- | | `read` | `GET /v1/balance`, `GET /v1/transactions`, `GET /v1/stablecoin/address` | | `pay` | `POST /v1/pay` (fiat + USDC rails), `POST /v1/usdc/transfer`, `POST /v1/payments` | | `x402` | `POST /v1/pay` (x402 rail), `POST /v1/x402/pay` | When creating a token, select only the permissions required: ```bash theme={null} curl -X POST https://api.unwall.xyz/dashboard/projects/PROJECT_ID/tokens \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "read-only-monitor", "permissions": ["read"] }' ``` If a token lacks the required permission for an endpoint, the API returns `403 Forbidden` with a message indicating which permission is missing. ## Rate Limits Agent API tokens are rate-limited to **100 requests per minute** using a sliding window. When the limit is exceeded, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating how many seconds to wait. ```json theme={null} { "detail": "Rate limit exceeded. Try again in 12 seconds." } ``` ## Error Responses | Status Code | Meaning | When It Happens | | ----------------------- | ------------------------ | ------------------------------------------------------------------------------------- | | `401 Unauthorized` | Invalid or missing token | The `Authorization` header is absent, malformed, or contains a revoked/invalid token. | | `403 Forbidden` | Insufficient permissions | The token is valid but does not have the required permission scope for the endpoint. | | `429 Too Many Requests` | Rate limited | The token has exceeded 100 requests per minute. Check the `Retry-After` header. | ### Example error response ```json theme={null} { "detail": "Token does not have the required permission: pay" } ``` ## Security Best Practices Never hard-code tokens in source code or commit them to version control. Use environment variables or a secrets manager like AWS Secrets Manager, HashiCorp Vault, or your platform's built-in secret store. Only grant the permissions your agent actually needs. A monitoring agent should have `read` only. A payment agent might need `read` + `pay`. Only grant `x402` to agents that call x402-enabled APIs. Create new tokens and revoke old ones on a regular cadence. You can have multiple active tokens per project, making zero-downtime rotation straightforward. When creating tokens, set an expiration date for short-lived use cases. Expired tokens are automatically rejected without needing manual revocation. # Payments Source: https://docs.unwall.xyz/concepts/payments Three payment rails unified under a single endpoint. # Payments Unwall provides three payment rails -- x402 protocol, on-chain USDC transfer, and fiat ACH -- all accessible through a single endpoint. The `POST /v1/pay` endpoint auto-routes to the correct rail based on the `recipient` field. ## Unified Endpoint ``` POST /v1/pay ``` The endpoint inspects the `recipient` value and routes automatically: | Recipient Format | Rail | Example | | -------------------------- | ------------- | ----------------------------------------------------- | | URL (`https://...`) | x402 Protocol | `"https://api.example.com/v1/data"` | | Ethereum address (`0x...`) | USDC Transfer | `"0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"` | | Object with bank details | Fiat ACH | `{"name": "Acme Corp", "account_number": "...", ...}` | No rail selection logic is needed in your agent. Just provide the recipient and Unwall handles the rest. ## Three Payment Rails ### x402 Protocol Pay for API calls in USDC using the HTTP 402 payment standard. When the recipient is a URL: 1. Unwall proxies your request to the target API. 2. If the API returns HTTP 402 with payment requirements, Unwall parses the payment details. 3. Unwall signs a USDC authorization (EIP-3009) and retries the request with payment attached. 4. The API response is returned to your agent. If the target API responds normally (no 402), the response is passed through at no charge. Set `max_amount_usdc` to cap how much your agent can spend per x402 call. See the [x402 Protocol guide](/guides/x402-payments) for details. ### USDC Transfer Send USDC on-chain to any Ethereum address on Base. The transfer is executed through Bridge.xyz and recorded in the ledger. ### Fiat ACH Convert USDC to USD and send via ACH bank transfer. Provide the recipient's name, account number, routing number, and email. Bridge.xyz handles the off-ramp conversion and transfer. ACH transfers are not instant. Expect 2-3 business days for settlement. ## Fee Model Fees are tier-based and charged **on top of** the payment amount: | Plan | Fee Rate | | ------------ | -------- | | **Free** | 2% | | **Pro** | 1.5% | | **Business** | 1% | For example, if you send a $100 payment on the Pro plan, $101.50 is debited from your wallet ($100 payment + $1.50 fee). The fee is recorded as a `PLATFORM_FEE` transaction in the ledger. ## Idempotency Include an `idempotency_key` in your request to prevent duplicate payments. If a request is retried with the same key, the original transaction is returned without creating a new one. ```json theme={null} { "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "amount_usd": 50.00, "idempotency_key": "invoice-1234-payment" } ``` Idempotency keys are unique per project. The same key can be used in different projects without conflict. ## Transaction Lifecycle Every payment goes through a defined set of statuses: ``` pending → processing → completed ↘ failed ↘ reversed ``` | Status | Meaning | | ------------ | ---------------------------------------------------------------------- | | `pending` | Payment recorded, not yet submitted to the payment provider | | `processing` | Submitted to Bridge.xyz or x402 facilitator, awaiting confirmation | | `completed` | Payment settled successfully | | `failed` | Payment failed. Balance is automatically credited back to the project. | | `reversed` | Payment was reversed after completion (e.g., ACH return) | ## Safety Guarantees Balance is debited using ledger RPC functions with PostgreSQL advisory locks for per-project serialization. Two concurrent payment requests cannot both succeed when only enough balance exists for one. The project balance is never served from cache when authorizing a payment. It is always fetched directly from the ledger to prevent overdraw. If a payment fails after the balance has been debited, the amount is automatically credited back to the project. If the rollback itself fails, the error is captured in Sentry for manual resolution. For x402 payments, the `max_amount_usdc` field ensures an agent never pays more than intended for a single API call. ## Deprecated Endpoints The following endpoints still work but `POST /v1/pay` is the recommended replacement: | Deprecated Endpoint | Rail | Replacement | | ------------------------ | ------------- | ------------------------------------------ | | `POST /v1/payments` | Fiat ACH | `POST /v1/pay` with bank details recipient | | `POST /v1/x402/pay` | x402 Protocol | `POST /v1/pay` with URL recipient | | `POST /v1/usdc/transfer` | USDC Transfer | `POST /v1/pay` with 0x address recipient | ## Next Steps Step-by-step guide with code examples for all three rails. Deep dive into how x402 payments work under the hood. # Projects Source: https://docs.unwall.xyz/concepts/projects Each project is an isolated wallet with its own balance, tokens, and transaction history. # Projects A **project** is the core organizational unit in Unwall. Each project is an isolated environment that contains a USDC wallet on Base chain, a set of API tokens, and a complete transaction ledger. There is no crossover between projects -- an agent operating in Project A cannot access funds or tokens belonging to Project B. ## What Is a Project? Every project you create gives you three things: A custodial wallet on Base chain, powered by Bridge.xyz. This is where your agent's funds live. Scoped bearer tokens that your agents use to authenticate. Each token has granular permissions. A complete history of every deposit, payment, and fee recorded via double-entry accounting. ## Balance Model Every project tracks these balance fields (all values in micro-USDC, where 1 USDC = 1,000,000): | Field | Type | Description | | -------------- | ------- | ------------------------------------------------- | | `available` | integer | USDC the agent can spend right now | | `pending` | integer | USDC in transit or awaiting on-chain confirmation | | `total_funded` | integer | Cumulative USDC funded into this project | | `total_spent` | integer | Cumulative USDC spent from this project | **The Supabase ledger is the source of truth for all balances.** Balance is computed from completed transaction rows via `ledger_get_balance`. Balance is never served from cache when authorizing payments -- it is always fetched fresh to prevent overdraw. ### Atomic Operations Balance modifications use ledger RPC functions with PostgreSQL advisory locks (`pg_advisory_xact_lock`) for per-project serialization. If two concurrent requests attempt to spend the last dollar, only one succeeds. The other receives an "Insufficient balance" error. ## Wallet Address Each project gets a **Bridge.xyz custodial wallet address** on Base chain for receiving USDC deposits. You can retrieve this address from the dashboard or via the API: ```bash theme={null} curl https://api.unwall.xyz/v1/stablecoin/address \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```json Response theme={null} { "chain": "base", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "currency": "usdc" } ``` Only send **USDC on the Base network** to this address. Sending other tokens or using the wrong chain may result in lost funds. ## Project Status | Status | Meaning | | -------- | -------------------------------------------------------------------------------- | | `active` | Fully operational. Agents can read balances, send payments, and make x402 calls. | | `paused` | Temporarily suspended. All API tokens for this project return 403 errors. | | `closed` | Permanently deactivated. No further operations allowed. | When a project is paused or closed, the token authentication middleware checks the project status and rejects requests before they reach the service layer. ## Project Limits The number of projects you can create depends on your plan tier: | Tier | Monthly Volume | Max Projects | Transaction Fee | | ----------- | ---------------- | ------------ | --------------- | | **Starter** | ≤ \$2,500 | 3 | 2% | | **Growth** | $2,500 – $10,000 | 10 | 1.5% | | **Scale** | > \$10,000 | Unlimited | 1% | Tiers are computed automatically from your monthly transaction volume — no subscriptions required. If your volume drops, existing projects remain active. You cannot create new projects until you are under the limit for your current tier. ## Creating a Project Projects are created through the dashboard at [app.unwall.xyz](https://app.unwall.xyz). Each new project is automatically provisioned with: 1. A Bridge.xyz custodial wallet on Base chain 2. An initial API token (displayed once at creation) 3. A ledger account for double-entry balance tracking ## Next Steps Add USDC to your project via on-chain deposit or virtual bank account. Create scoped tokens with the right permissions for your agents. # API Tokens Source: https://docs.unwall.xyz/concepts/tokens Create scoped tokens with granular permissions to control what your agents can do. # API Tokens API tokens are the credentials that AI agents use to authenticate with Unwall. Each token is scoped to a single project and carries a specific set of permissions that control what the agent can do. ## Token Format Tokens follow this format: ``` aw_live_ ``` * **Prefix:** `aw_live_` identifies the token as an Unwall token. The prefix is stored in the database for display purposes (e.g., showing `aw_live_a1b2...` in the dashboard). * **Body:** A cryptographically random string generated at creation time. The full token is displayed **only once** at creation time. It cannot be retrieved again. If lost, revoke the old token and create a new one. ## Permissions Each token carries an array of permissions that control which API endpoints the agent can access: | Permission | Grants Access To | | ---------- | ----------------------------------------------------------------------- | | `read` | `GET /v1/balance`, `GET /v1/transactions`, `GET /v1/stablecoin/address` | | `pay` | `POST /v1/pay`, `POST /v1/payments`, `POST /v1/usdc/transfer` | | `x402` | `POST /v1/pay` (x402 rail), `POST /v1/x402/pay` | Permissions are checked at the endpoint level. If a token lacks the required permission, the request is rejected with a `403 Forbidden` error: ```json theme={null} { "detail": "Token lacks required permission: pay" } ``` Follow the principle of least privilege. If an agent only needs to check balances and view transactions, issue a token with only the `read` permission. Create separate tokens for different agents or services with minimal permissions. ## Security Model Tokens are never stored in plaintext. The system uses a one-way **SHA-256 hash** for storage and lookup: The plaintext token is returned to the user exactly once at creation time. The SHA-256 hash of the token is stored in the `api_tokens` table. The original plaintext is discarded. When an agent sends a request, the bearer token is hashed and matched against stored hashes. This means that even if the database is compromised, an attacker cannot reconstruct valid tokens from the stored hashes. ## Expiry and Revocation Tokens support two mechanisms for invalidation: * **Optional expiration date**: Set an expiry when creating the token. After the expiry date, the token is automatically rejected. * **Manual revocation**: Revoke any token at any time from the dashboard. The token's `is_active` flag is set to `false`. There is a window of up to 5 minutes after revocation during which a cached token may still be accepted, due to Redis caching. For time-sensitive revocations, the cache entry expires naturally within this window. ## Rate Limiting Every token is rate-limited to **100 requests per minute** using a Redis-backed sliding window algorithm. * The rate limiter uses Redis sorted sets to track request timestamps within a 60-second window. * When the limit is exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header. * If Redis is unavailable, the rate limiter degrades gracefully and allows requests through (fail-open). ```json theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 12 { "detail": "Rate limit exceeded. Try again later." } ``` ## Redis Caching To avoid hitting the database on every request, validated token contexts are cached in Redis for **5 minutes** (300 seconds): 1. Agent sends a request with `Authorization: Bearer aw_live_...`. 2. The token is SHA-256 hashed. 3. Redis is checked for the cache key `token:`. 4. **Cache hit:** The stored context (token ID, project ID, user ID, permissions) is returned immediately. 5. **Cache miss:** The hash is looked up in the database, the project status is verified, and the result is cached. The cached context does **not** include the project balance. Balance is always fetched fresh from the ledger when needed for payment authorization. This prevents agents from exploiting stale cached balances to overdraw. ## Token Lifecycle | Action | How | | ---------- | ------------------------------------------------------------------------------------------------- | | **Create** | Via dashboard: `POST /dashboard/projects/:id/tokens` | | **List** | Via dashboard: `GET /dashboard/projects/:id/tokens` (returns metadata only, never the full token) | | **Revoke** | Via dashboard: `DELETE /dashboard/projects/:id/tokens/:token_id` | ## Token Fields | Field | Type | Description | | -------------- | ---------------- | -------------------------------------------------- | | `id` | string | Unique token identifier | | `token_prefix` | string | Display prefix (e.g., `aw_live_a1b2`) | | `name` | string | Human-readable label | | `permissions` | array | List of permissions: `read`, `pay`, `x402` | | `is_active` | boolean | Whether the token is currently valid | | `expires_at` | datetime or null | Optional expiration date | | `last_used_at` | datetime | Last time the token was used (updated best-effort) | | `created_at` | datetime | When the token was created | # Webhooks Source: https://docs.unwall.xyz/concepts/webhooks Real-time notifications for deposits and transfer events. # Webhooks Unwall receives webhooks from **Bridge.xyz** to process deposits and transfer status updates in real time. All webhook endpoints verify cryptographic signatures before processing any event. These are platform-internal webhooks (Bridge.xyz sends events to Unwall). Developer-facing webhooks -- where Unwall notifies your application of events -- are on the roadmap. ## Bridge.xyz Events Bridge.xyz webhooks handle USDC deposits and outgoing transfer status updates. | Event | Action | | -------------------- | ------------------------------------------------------------------------------------------------------------------ | | `deposit.confirmed` | USDC deposit received on-chain. The project's USDC balance is credited via a ledger posting. | | `transfer.completed` | Outgoing transfer (USDC or fiat off-ramp) settled successfully. Transaction status updated to `completed`. | | `transfer.failed` | Outgoing transfer failed. Transaction marked as `failed` and the amount is credited back to the project's balance. | Bridge.xyz webhook signatures are verified using the `BRIDGE_WEBHOOK_PUBLIC_KEY` before any balance updates are applied. ## Error Handling If the webhook signature is invalid, the endpoint returns `400 Bad Request` and logs the failure. The event is not processed. If an error occurs while processing a valid webhook, the endpoint returns `500 Internal Server Error`. The payment provider retries the webhook according to its retry policy. Unrecognized event types are logged and acknowledged with `200 OK` to prevent unnecessary retries. # Fund Your Project Source: https://docs.unwall.xyz/guides/fund-project Add USDC to your project wallet via on-chain deposit or virtual bank account. # Fund Your Project Before your agent can make payments, the project needs a funded USDC balance. There are two ways to add funds: deposit USDC directly on-chain (recommended) or send fiat via a virtual bank account. ## Method 1: On-Chain USDC Deposit The fastest way to fund your project. Send USDC on the Base network directly to your project's wallet address. ### Get Your Deposit Address Retrieve your project's deposit address via the API or from the project dashboard at [app.unwall.xyz](https://app.unwall.xyz). ```bash cURL theme={null} curl https://api.unwall.xyz/v1/stablecoin/address \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```python Python theme={null} import requests headers = {"Authorization": "Bearer aw_live_xxxxxxxxxxxx"} response = requests.get("https://api.unwall.xyz/v1/stablecoin/address", headers=headers) print(response.json()) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/stablecoin/address", { headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx" }, }); const address = await response.json(); console.log(address); ``` ```json Response theme={null} { "chain": "base", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "currency": "usdc" } ``` ### Send USDC Send USDC to the returned address on the **Base** network. You can send from any wallet, exchange, or bridge that supports Base. Copy the `address` from the API response or the project dashboard. From your wallet or exchange, send USDC to the address. Make sure you select the **Base** network. Deposits are confirmed via Bridge.xyz webhook, usually within a few minutes. Your project balance updates automatically. Only send **USDC on the Base network** to this address. Sending other tokens or using the wrong network may result in lost funds. ## Method 2: Virtual Bank Account (Fiat On-Ramp) If you prefer to fund with USD, you can create a virtual bank account that automatically converts incoming deposits to USDC. From the project dashboard, navigate to **Funding** and create a virtual bank account. Bridge.xyz provisions a US bank account linked to your project. Send USD to the virtual bank account from your bank. Both ACH transfers and wire transfers are supported. Bridge.xyz automatically converts the incoming USD to USDC and credits your project wallet on Base chain. ACH deposits typically take 1-3 business days to settle. Wire transfers are usually same-day. Your project balance updates once Bridge.xyz confirms the conversion. ## Checking Your Balance After funding, verify your balance: ```bash cURL theme={null} curl https://api.unwall.xyz/v1/balance \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```python Python theme={null} import requests headers = {"Authorization": "Bearer aw_live_xxxxxxxxxxxx"} response = requests.get("https://api.unwall.xyz/v1/balance", headers=headers) print(response.json()) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/balance", { headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx" }, }); const balance = await response.json(); console.log(balance); ``` ```json Response theme={null} { "project_id": "proj_abc123", "currency": "usdc", "available": 5000000, "pending": 0, "total_funded": 5000000, "total_spent": 0, "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28" } ``` All amounts are in micro-USDC (1 USDC = 1,000,000). So `5000000` = 5.00 USDC. ## Processing Times | Method | Typical Time | | --------------------------- | ------------------------------------- | | On-chain USDC deposit | Minutes (after on-chain confirmation) | | Virtual bank account (ACH) | 1-3 business days | | Virtual bank account (Wire) | Same day | ## Next Steps Use your funded balance to make payments via x402, USDC, or ACH. Pay for API calls automatically with your USDC balance. # MCP Server Source: https://docs.unwall.xyz/guides/mcp-server Connect Unwall to Claude, Cursor, and other MCP-compatible AI clients. # MCP Server The Unwall MCP server lets AI assistants like Claude, Cursor, and other MCP-compatible clients interact with your Unwall project directly -- checking balances, sending payments, and making x402 API calls through natural language. ## What Is MCP? The **Model Context Protocol (MCP)** is an open standard that lets AI assistants use external tools. Instead of writing code to call APIs, your AI assistant can use MCP tools as native capabilities. For example, you can ask Claude "What's my wallet balance?" and it will call the `get_balance` tool automatically. ## Installation No global install is needed. The MCP server runs via `npx`: ```bash theme={null} npx @unwall/mcp-server ``` ## Claude Desktop Configuration Add the following to your Claude Desktop configuration file (`claude_desktop_config.json`): ```json theme={null} { "mcpServers": { "unwall": { "command": "npx", "args": ["@unwall/mcp-server"], "env": { "UNWALL_API_KEY": "aw_live_your_key" } } } } ``` On macOS, the Claude Desktop config file is located at `~/Library/Application Support/Claude/claude_desktop_config.json`. On Windows, it is at `%APPDATA%\Claude\claude_desktop_config.json`. ## Cursor / VS Code Add the MCP server in your editor's MCP settings. The configuration is the same JSON format: ```json theme={null} { "mcpServers": { "unwall": { "command": "npx", "args": ["@unwall/mcp-server"], "env": { "UNWALL_API_KEY": "aw_live_your_key" } } } } ``` ## Available Tools The MCP server exposes the following tools to your AI assistant: | Tool | Description | API Equivalent | | -------------------------- | ----------------------------- | ---------------------------- | | `get_balance` | Check wallet balance | `GET /v1/balance` | | `list_transactions` | View transaction history | `GET /v1/transactions` | | `pay` | Make a payment (any rail) | `POST /v1/pay` | | `get_usdc_deposit_address` | Get funding address | `GET /v1/stablecoin/address` | | `send_payment` | Send ACH payment (deprecated) | `POST /v1/payments` | | `x402_pay` | x402 API call (deprecated) | `POST /v1/x402/pay` | | `send_usdc` | USDC transfer (deprecated) | `POST /v1/usdc/transfer` | The `pay` tool is the recommended way to make payments. It supports all three rails (x402, USDC transfer, fiat ACH) and auto-routes based on the recipient. The `send_payment`, `x402_pay`, and `send_usdc` tools are deprecated but still functional. ## Environment Variables | Variable | Required | Default | Description | | ----------------- | -------- | ------------------------ | ------------------------------------- | | `UNWALL_API_KEY` | Yes | -- | Your Unwall API token (`aw_live_...`) | | `UNWALL_BASE_URL` | No | `https://api.unwall.xyz` | API base URL (override for testing) | ## Example Usage Here is an example conversation with Claude using the Unwall MCP server: **You:** What's my current wallet balance? **Claude:** Let me check your Unwall balance. *Uses `get_balance` tool* Your project has **5.00 USDC** available (\$5.00 USD equivalent) with no pending transactions. **You:** Fetch the latest market data from [https://api.example.com/v1/market](https://api.example.com/v1/market) using x402. Limit spending to 0.50 USDC. **Claude:** I'll fetch the market data using an x402 payment. *Uses `pay` tool with recipient "[https://api.example.com/v1/market](https://api.example.com/v1/market)" and max\_amount\_usdc 500000* The request completed successfully. The API charged 0.05 USDC plus a 0.001 USDC platform fee. Here's the market data: ```json theme={null} {"data": [{"symbol": "BTC", "price": 95420.50}, ...]} ``` **You:** Send 10 USDC to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 for the vendor invoice. **Claude:** I'll send 10 USDC to that address. *Uses `pay` tool with recipient "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18" and amount\_usd 10.00* Payment submitted. Transaction ID: `tx_abc123`. The transfer is processing on Base chain and should complete within a few minutes. ## Token Permissions The MCP server inherits the permissions of the API token you provide. Make sure your token has the permissions your AI assistant needs: | Action | Required Permission | | -------------------------------- | ------------------- | | Check balance, view transactions | `read` | | Send USDC or fiat payments | `pay` | | Make x402 API calls | `x402` | For most use cases, create a token with all three permissions: `read`, `pay`, and `x402`. If your assistant only needs to monitor balances, a `read`-only token is sufficient and more secure. ## Next Steps Learn about token permissions and security best practices. Deep dive into how x402 payments work under the hood. # Send a Payment Source: https://docs.unwall.xyz/guides/send-payment Use the unified /v1/pay endpoint to send payments via x402, USDC, or fiat. # Send a Payment The `POST /v1/pay` endpoint is Unwall's unified payment interface. It accepts three types of recipients and automatically routes to the correct payment rail -- no rail selection logic needed in your agent code. | Recipient | Rail | Use Case | | ------------------- | ------------- | ---------------------------------------------- | | URL | x402 Protocol | Pay for API calls in USDC | | `0x...` address | USDC Transfer | Send USDC on-chain | | Bank details object | Fiat ACH | Convert USDC to USD and send via bank transfer | ## x402 Payment When the recipient is a URL, Unwall proxies the request and handles the x402 payment protocol automatically. ```bash cURL theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": "https://api.example.com/v1/data", "max_amount_usdc": 1000000, "description": "Fetch market data" }' ``` ```python Python theme={null} import requests headers = { "Authorization": "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", } payload = { "recipient": "https://api.example.com/v1/data", "max_amount_usdc": 1000000, "description": "Fetch market data", } response = requests.post("https://api.unwall.xyz/v1/pay", json=payload, headers=headers) print(response.json()) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: "https://api.example.com/v1/data", max_amount_usdc: 1000000, description: "Fetch market data", }), }); const result = await response.json(); console.log(result); ``` ```json Response theme={null} { "id": "tx_abc123", "status": "completed", "rail": "x402", "amount_charged": 50000, "fee": 1000, "currency": "usdc", "recipient": "https://api.example.com/v1/data", "tx_hash": "0xabc...", "response": { "status_code": 200, "body": "{\"data\": [...]}" } } ``` The `max_amount_usdc` field is in micro-USDC (1 USDC = 1,000,000). Setting it to `1000000` means the agent will pay up to 1 USDC for this call. If the API charges more, the payment is rejected. ## USDC Transfer When the recipient is an Ethereum address (`0x...`), Unwall sends USDC on Base chain via Bridge.xyz. ```bash cURL theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "amount_usd": 50.00, "description": "Vendor payment" }' ``` ```python Python theme={null} import requests headers = { "Authorization": "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", } payload = { "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "amount_usd": 50.00, "description": "Vendor payment", } response = requests.post("https://api.unwall.xyz/v1/pay", json=payload, headers=headers) print(response.json()) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", amount_usd: 50.0, description: "Vendor payment", }), }); const result = await response.json(); console.log(result); ``` ```json Response theme={null} { "id": "tx_def456", "status": "processing", "rail": "usdc_transfer", "amount": 50000000, "fee": 750000, "currency": "usdc", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "tx_hash": "0xdef..." } ``` ## Fiat ACH Payment When the recipient is an object with bank details, Unwall converts USDC to USD and sends via ACH bank transfer. ```bash cURL theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": { "name": "Acme Corp", "account_number": "123456789", "routing_number": "021000021", "email": "billing@acme.com" }, "amount_usd": 250.00, "description": "Invoice #1234" }' ``` ```python Python theme={null} import requests headers = { "Authorization": "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", } payload = { "recipient": { "name": "Acme Corp", "account_number": "123456789", "routing_number": "021000021", "email": "billing@acme.com", }, "amount_usd": 250.00, "description": "Invoice #1234", } response = requests.post("https://api.unwall.xyz/v1/pay", json=payload, headers=headers) print(response.json()) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: { name: "Acme Corp", account_number: "123456789", routing_number: "021000021", email: "billing@acme.com", }, amount_usd: 250.0, description: "Invoice #1234", }), }); const result = await response.json(); console.log(result); ``` ```json Response theme={null} { "id": "tx_ghi789", "status": "processing", "rail": "fiat_ach", "amount": 25000, "fee": 375, "currency": "usd", "recipient_name": "Acme Corp", "estimated_arrival": "2-3 business days" } ``` ACH transfers are not instant. Expect 2-3 business days for settlement. Use the `GET /v1/transactions` endpoint to track status changes. ## Idempotency Include an `idempotency_key` to prevent duplicate payments. If a request is retried with the same key, the original transaction is returned without creating a new one. ```json theme={null} { "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "amount_usd": 50.00, "idempotency_key": "invoice-1234-payment", "description": "Vendor payment" } ``` This is especially important for AI agents that may retry requests on network errors or timeouts. Idempotency keys are scoped to each project -- the same key can be used in different projects without conflict. ## Error Handling | Status Code | Meaning | Example | | ----------- | ------------------- | ----------------------------------------------------------------------- | | `400` | Validation error | Missing required fields, insufficient balance, invalid recipient format | | `403` | Permission denied | Token lacks `pay` or `x402` permission, project is paused | | `429` | Rate limit exceeded | More than 100 requests per minute | | `502` | Upstream failure | Bridge.xyz or x402 facilitator returned an error | ```json Example error response theme={null} { "detail": "Insufficient balance. Available: 1000000, Required: 5000000" } ``` Always check the `status` field in the response. A `200` response with `"status": "processing"` means the payment was accepted but has not yet settled. Use `GET /v1/transactions` or wait for webhook confirmation to verify completion. ## Required Permissions | Rail | Required Permission | | ------------- | ------------------- | | x402 Protocol | `x402` | | USDC Transfer | `pay` | | Fiat ACH | `pay` | # x402 Protocol Source: https://docs.unwall.xyz/guides/x402-payments Pay for API calls automatically in USDC using the HTTP 402 payment standard. # x402 Protocol The x402 protocol uses HTTP 402 Payment Required as a machine-to-machine payment mechanism. APIs that support x402 return a 402 response with payment requirements instead of requiring API keys or subscriptions. Unwall handles the entire payment flow -- your agent just makes a request and gets data back. ## What Is x402? Traditional API monetization requires signing up, generating API keys, and managing subscriptions. x402 replaces all of that with a single HTTP flow: 1. Client requests data from an API. 2. API returns `402 Payment Required` with the price and payment address. 3. Client signs a USDC payment and retries the request. 4. API verifies payment on-chain and returns the data. Unwall acts as the payment intermediary, so your agent never needs to manage wallets or sign transactions directly. ## How It Works The agent provides the target API URL and a `max_amount_usdc` safety cap. The initial request is sent to the URL exactly as if your agent called it directly. The 402 response contains the price, payment address, and token details in a structured format. Using its platform signing key, Unwall creates an EIP-3009 `transferWithAuthorization` signature for the required USDC amount. The original request is sent again with an `X-PAYMENT` header containing the signed authorization. The target API verifies the payment via a facilitator, settles on-chain, and returns data. Unwall passes the response back to your agent. ## Safety Cap Always set `max_amount_usdc` to limit how much your agent can spend on a single x402 call. If the API's price exceeds this cap, the payment is rejected and the request fails with an error -- no funds are spent. ```json theme={null} { "recipient": "https://api.example.com/v1/data", "max_amount_usdc": 500000, "description": "Fetch market data" } ``` In this example, the agent will pay up to 0.50 USDC (500,000 micro-USDC). If the API charges more, the request is rejected. Without `max_amount_usdc`, an agent could be charged an unexpectedly high amount by a malicious or misconfigured API. Always set this field. ## When No 402 Is Returned If the target API responds with a normal HTTP response (200, 301, etc.) instead of 402, the response is passed through directly to your agent at no charge. No USDC is spent and no fee is applied. This means you can safely point the x402 endpoint at any URL. If the API does not use the x402 protocol, it works as a simple proxy. ## Code Examples ```bash cURL theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": "https://api.example.com/v1/data", "max_amount_usdc": 1000000, "method": "GET", "description": "Fetch market data via x402" }' ``` ```python Python theme={null} import requests headers = { "Authorization": "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", } payload = { "recipient": "https://api.example.com/v1/data", "max_amount_usdc": 1000000, "method": "GET", "description": "Fetch market data via x402", } response = requests.post("https://api.unwall.xyz/v1/pay", json=payload, headers=headers) result = response.json() # The API response body is included in the result print(result["response"]["body"]) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: "https://api.example.com/v1/data", max_amount_usdc: 1000000, method: "GET", description: "Fetch market data via x402", }), }); const result = await response.json(); // The API response body is included in the result console.log(result.response.body); ``` ```json Response theme={null} { "id": "tx_abc123", "status": "completed", "rail": "x402", "amount_charged": 50000, "fee": 1000, "currency": "usdc", "recipient": "https://api.example.com/v1/data", "tx_hash": "0xabc...", "response": { "status_code": 200, "body": "{\"data\": [...]}" } } ``` ## Request Fields | Field | Type | Required | Description | | ----------------- | ------- | -------- | ------------------------------------------------------ | | `recipient` | string | Yes | Target API URL (must start with `https://`) | | `max_amount_usdc` | integer | Yes | Maximum USDC to pay in micro-USDC (1 USDC = 1,000,000) | | `method` | string | No | HTTP method: GET, POST, PUT, DELETE. Default: GET | | `headers` | object | No | Additional headers to forward to the target API | | `body` | string | No | Request body for POST/PUT requests | | `description` | string | No | Human-readable description for the transaction ledger | | `idempotency_key` | string | No | Unique key to prevent duplicate payments | ## Fee The tier-based platform fee applies on successful x402 payments: | Plan | Fee Rate | | -------- | -------- | | Free | 2% | | Pro | 1.5% | | Business | 1% | The fee is charged on top of the x402 payment amount and recorded as a `PLATFORM_FEE` transaction in the ledger. ## Required Permission Your API token must have the `x402` permission to make x402 payments. Tokens with only `pay` permission cannot use the x402 rail. ## Next Steps See examples of all three payment rails in one guide. Give your AI assistant direct access to x402 payments via MCP tools. # Introduction Source: https://docs.unwall.xyz/introduction Give your AI agents the ability to make payments with USDC wallets, multi-rail routing, and the x402 protocol. # Unwall Unwall is a payment infrastructure platform built for AI agents. One API gives your agents access to three payment rails -- x402 protocol payments, on-chain USDC transfers, and fiat ACH -- all routed automatically through a single endpoint. One endpoint routes to x402, USDC transfer, or fiat ACH based on the recipient. No rail selection logic needed in your agent. Every project gets a USDC wallet on Base chain, custodied by Bridge.xyz. Fund it on-chain or via fiat deposit. Pay-per-API-call with automatic USDC settlement. Your agent pays for x402-enabled services directly, with no pre-negotiation or subscriptions. Native integration with Claude, Cursor, and other MCP clients via the `@unwall/mcp-server` package. ## How It Works Sign up at [app.unwall.xyz](https://app.unwall.xyz) and create a project. Each project is an isolated wallet with its own balance and transaction history. Get your project's deposit address and send USDC on the Base network. Funds appear in your balance once the transaction confirms on-chain. Create a token with the permissions your agent needs: `read`, `pay`, and `x402`. The token looks like `aw_live_...` and is shown only once at creation time. Your agent calls a single endpoint with a recipient. Unwall auto-routes to the right payment rail -- x402 for protocol-enabled URLs, USDC for on-chain addresses, or fiat for bank account recipients. Everything your agent needs is accessible through a single `Authorization: Bearer aw_live_...` header. ## Next Steps Start making payments from your AI agent in under 5 minutes. Explore every endpoint with request and response examples. Set up the MCP server for Claude, Cursor, and other MCP clients. # Pricing Source: https://docs.unwall.xyz/pricing Usage-based pricing that scales automatically with your payment volume. ## Usage-Based Auto-Tiering Unwall uses usage-based pricing with automatic tier progression. There are no subscriptions and no credit card required. Your tier is determined by your monthly outbound payment volume, and it updates automatically as your volume grows. | | Starter | Growth | Scale | | ------------------- | ----------- | ---------------- | --------- | | **Monthly Volume** | $0 - $2,500 | $2,500 - $10,000 | \$10,000+ | | **Transaction Fee** | 2% | 1.5% | 1% | | **Projects** | 3 | 10 | Unlimited | All tiers include access to every payment rail: x402 protocol, on-chain USDC transfers, and fiat ACH payments. ## How Volume Is Calculated Monthly volume is the sum of all **outbound payments** processed through your projects during the current calendar month. This includes: * Agent-initiated payments (via API tokens) * USDC transfers * x402 protocol payments Inbound deposits (USDC deposits, fiat funding) do **not** count toward your volume. Volume resets to \$0 at the start of each calendar month (UTC). ## How Fees Work The platform fee is calculated as a percentage of the payment amount in micro-USDC (1 USDC = 1,000,000 micro-USDC). ``` Fee = ceil(amount x fee_rate) ``` * **Minimum fee**: 1 micro-USDC per transaction. * The fee is charged as a separate `PLATFORM_FEE` transaction in your project's ledger, recorded alongside the payment itself. * The fee is added **on top of** the payment amount -- the recipient receives the full requested amount. ### Example On the **Growth** tier (1.5% fee), sending 100,000 micro-USDC (0.10 USDC): ``` Fee = ceil(100,000 x 0.015) = ceil(1,500) = 1,500 micro-USDC Total deducted from wallet = 100,000 + 1,500 = 101,500 micro-USDC Recipient receives = 100,000 micro-USDC ``` Bridge.xyz charges a separate 1% processing fee on fiat off-ramp transfers. This is independent of the Unwall platform fee. ## Mid-Month Tier Transitions Your tier updates in real time as your monthly volume crosses thresholds: * When your volume exceeds **\$2,500**, your fee rate automatically drops from 2% to 1.5% and your project limit increases to 10. * When your volume exceeds **\$10,000**, your fee rate drops to 1% and your project limit becomes unlimited. The new fee rate applies to all transactions **after** the threshold is crossed. Previously processed transactions in that month retain their original fee. ## Volume Drops If your volume is lower in a subsequent month: * Your tier resets based on the new month's volume (starting at Starter). * **Existing projects stay active.** You will not lose access to any project or its balance. * **New project creation is blocked** if the number of your existing projects exceeds the limit for your current tier. For example, if you had 8 projects during a Growth month and drop back to Starter (3 projects), you cannot create new projects until you are at or below the limit. ## Viewing Your Tier You can see your current tier, monthly volume, and progress toward the next tier in the **Settings** page of the [Unwall dashboard](https://app.unwall.xyz). # Quickstart Source: https://docs.unwall.xyz/quickstart Start making payments from your AI agent in under 5 minutes. ## Step 1: Create an Account 1. Go to [app.unwall.xyz](https://app.unwall.xyz) and sign up. 2. Complete identity verification (KYC). 3. Create your first project -- give it a name like "My Agent" and optionally set a budget limit. Each project is an isolated wallet with its own USDC balance, transaction history, and API tokens. ## Step 2: Get Your API Token From your project's dashboard page: 1. Navigate to **Tokens** and click **Create Token**. 2. Select the permissions your agent needs: `read`, `pay`, and `x402`. 3. Copy the token immediately -- it starts with `aw_live_` and is only displayed once. Store your token securely. The full token value cannot be retrieved after creation. If you lose it, revoke the old token and create a new one. ## Step 3: Fund Your Wallet Get your project's USDC deposit address: ```bash theme={null} curl https://api.unwall.xyz/v1/stablecoin/address \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```json Response theme={null} { "chain": "base", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28", "currency": "usdc" } ``` Send USDC on the **Base** network to the returned address. Your balance will update once the on-chain transaction confirms. ## Step 4: Make Your First API Call ### Check your balance ```bash cURL theme={null} curl https://api.unwall.xyz/v1/balance \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" ``` ```python Python theme={null} import requests headers = {"Authorization": "Bearer aw_live_xxxxxxxxxxxx"} response = requests.get("https://api.unwall.xyz/v1/balance", headers=headers) print(response.json()) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/balance", { headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx" }, }); const balance = await response.json(); console.log(balance); ``` ```json Response theme={null} { "project_id": "proj_abc123", "currency": "usdc", "available": 5000000, "pending": 0, "total_funded": 5000000, "total_spent": 0, "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18" } ``` ### Send a payment via x402 Use the unified `/v1/pay` endpoint. When the recipient is an x402-enabled URL, Unwall automatically routes via the x402 protocol. ```bash cURL theme={null} curl -X POST https://api.unwall.xyz/v1/pay \ -H "Authorization: Bearer aw_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient": "https://api.example.com/v1/data", "amount": 10000, "description": "Fetch market data via x402" }' ``` ```python Python theme={null} import requests headers = { "Authorization": "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", } payload = { "recipient": "https://api.example.com/v1/data", "amount": 10000, "description": "Fetch market data via x402", } response = requests.post("https://api.unwall.xyz/v1/pay", json=payload, headers=headers) print(response.json()) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.unwall.xyz/v1/pay", { method: "POST", headers: { Authorization: "Bearer aw_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: "https://api.example.com/v1/data", amount: 10000, description: "Fetch market data via x402", }), }); const result = await response.json(); console.log(result); ``` ```json Response theme={null} { "id": "tx_def456", "status": "completed", "rail": "x402", "amount": 10000, "recipient": "https://api.example.com/v1/data", "created_at": "2026-03-11T10:30:00Z" } ``` ## Step 5: Set Up MCP (Optional) If you use Claude Desktop, Cursor, or another MCP-compatible client, you can give your AI assistant direct access to Unwall tools. Add the following to your Claude Desktop configuration (`claude_desktop_config.json`): ```json theme={null} { "mcpServers": { "unwall": { "command": "npx", "args": ["-y", "@unwall/mcp-server"], "env": { "UNWALL_API_TOKEN": "aw_live_xxxxxxxxxxxx" } } } } ``` This gives your MCP client access to tools like `get_balance`, `send_payment`, `send_usdc`, `x402_pay`, and more. ## What's Next? Learn about token permissions, rate limits, and security best practices. Deep dive into the POST /v1/pay endpoint and its routing logic. Understand how x402 protocol payments work under the hood. Full setup guide for the MCP server with all available tools. # Security Source: https://docs.unwall.xyz/security/overview How Unwall protects your funds and data. # Security Unwall is built for handling real money and on-chain USDC payments. Security is enforced at every layer -- from token authentication to double-entry accounting. ## Token Security | Measure | Detail | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | **SHA-256 hashing** | Plaintext tokens are never stored. Only the SHA-256 hash is saved in the database. | | **One-time display** | Full tokens are shown once at creation and cannot be retrieved again. | | **Scoped permissions** | Tokens carry independent `read`, `pay`, and `x402` permissions. Agents only get the access you explicitly grant. | | **Optional expiry** | Tokens can be configured with an expiration date for time-limited access. | | **Instant revocation** | Revoked tokens are rejected within the Redis cache TTL (up to 5 minutes). | | **Rate limiting** | 100 requests per minute per token via Redis sliding window. Stricter limits apply to expensive operations like payments. | ## USDC Custody | Measure | Detail | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **Bridge.xyz custodial wallets** | All USDC funds are held in Bridge.xyz custodial wallets on Base chain with institutional-grade custody. | | **On-chain verification** | USDC deposits are verified on-chain before crediting project balances. Bridge.xyz webhooks confirm deposit finality. | | **Chain validation** | Deposit addresses are chain-specific. The platform validates that USDC is sent on the correct network. | | **EIP-3009 signing** | x402 payments use EIP-3009 `transferWithAuthorization` signatures. The platform signing key is stored securely in environment variables. | ## Rate Limiting Rate limiting uses a per-token sliding window backed by Redis sorted sets: * **Standard limit:** 100 requests per minute per token. * **Payment operations:** Stricter limits to prevent rapid-fire spending. * **Fail-open:** If Redis is unavailable, requests are allowed through to avoid blocking legitimate traffic. * **429 responses** include a `Retry-After` header indicating when the client can retry. ## Webhook Verification All incoming webhooks are verified before processing: | Provider | Verification Method | | -------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Bridge.xyz** | Webhook signatures verified using the `BRIDGE_WEBHOOK_PUBLIC_KEY`. Invalid signatures are rejected with 400. | | **Stripe** | Webhook signatures verified using the Stripe SDK with `STRIPE_WEBHOOK_SECRET`. Invalid signatures are rejected with 400. | Events with invalid signatures are logged but never processed. Unknown event types are acknowledged with 200 to prevent unnecessary retries. ## Idempotency Duplicate payment protection is enforced at multiple levels: * **Idempotency keys**: Clients include an `idempotency_key` field to prevent duplicate payments from retries or agent loops. * **Database constraints**: Unique constraints on idempotency keys per project ensure that even concurrent duplicate requests result in only one payment. * **Atomic balance operations**: Ledger postings are atomic (via `pg_advisory_xact_lock`), preventing double-spend from race conditions. ## Double-Entry Accounting **The Supabase ledger is the authoritative source of truth for all balances.** Every financial operation -- deposit, payment, fee, refund -- is recorded as an immutable ledger posting with balanced debits and credits. Balance is computed from completed transaction rows via `ledger_get_balance`. This design ensures: * **Auditability**: Every balance change has a corresponding ledger entry that can be traced. * **Consistency**: Double-entry accounting guarantees that funds cannot appear or disappear without a matching counterpart. * **Atomicity**: Ledger postings are atomic. A debit and its corresponding credit either both succeed or both fail. ## Infrastructure | Measure | Detail | | ------------------------- | --------------------------------------------------------------------------------------------------------- | | **HTTPS everywhere** | All API traffic is encrypted in transit. | | **Environment isolation** | Secrets are stored in environment variables, never in code. | | **Redis caching** | Token validation cached to reduce database load. Balance is never cached for payment authorization. | | **Error monitoring** | Sentry integration for real-time error tracking and alerting on critical failures. | | **Row Level Security** | Supabase RLS policies ensure users can only access their own data. Only the backend uses the service key. | ## Responsible Disclosure If you discover a security vulnerability, please contact [security@unwall.xyz](mailto:security@unwall.xyz).