> ## Documentation Index
> Fetch the complete documentation index at: https://docs.services.payward.com/llms.txt
> Use this file to discover all available pages before exploring further.

# On-chain swaps

> The on-chain swaps API lets you create a wallet-funded xStocks quote, submit the returned payload on-chain, and poll until the quote reaches a terminal state.

## Prerequisites

* Payward Services API credentials (see the [Authentication guide](/tabs/developer-documentation/get-started/authentication)).
* A verified user with at least one account and on-chain quote trading enabled.
* A source wallet registered on the user's withdrawal-address list as a same-owner address. The address must be approved before you create a quote.
* Enough source-asset balance in the wallet to settle the on-chain quote.

## Workflow

<Steps>
  <Step title="Create an on-chain quote">
    Create a wallet-funded xStocks on-chain quote and receive the on-chain payload.

    `POST /v1/accounts/{account_id}/onchain-quotes`
  </Step>

  <Step title="Submit the on-chain transaction">
    Call the Payward on-chain proxy contract with the returned payload before `expires_at`.

    `executeSwap(...)`
  </Step>

  <Step title="Poll for terminal status">
    Wait for Payward Services to observe settlement, or subscribe to webhooks.

    `GET /v1/accounts/{account_id}/onchain-quotes/{quote_id}`
  </Step>
</Steps>

## Quote lifecycle

| Status      | Description                                                                                                              |
| ----------- | ------------------------------------------------------------------------------------------------------------------------ |
| `pending`   | Quote is being prepared. Transitions to `offered` once the signed payload is ready, or to `failed` if preparation fails. |
| `offered`   | Quote created and locked. Valid until `expires_at`.                                                                      |
| `executing` | The on-chain transaction was observed and is awaiting confirmation.                                                      |
| `executed`  | Terminal. The on-chain quote completed.                                                                                  |
| `expired`   | Terminal. `expires_at` elapsed before settlement.                                                                        |
| `failed`    | Terminal. The on-chain transaction reverted, or the offer could not be completed.                                        |

## Specifying the trade

Onchain quotes are restricted to xStock and USDC pairs. Set the `wallet` block on `from` — the quote is funded from the user's wallet, not from the user's Payward Services account balance.

| Field                        | Required | Description                                                                      |
| ---------------------------- | -------- | -------------------------------------------------------------------------------- |
| `from.wallet`                | yes      | `{ address, network }`. Must be an approved same-owner withdrawal address.       |
| `from.symbol`                | yes      | Source asset symbol. Use `USDC` when buying xStocks, or the xStock when selling. |
| `from.type`                  | yes      | `stablecoin` or `xstock`.                                                        |
| `to.symbol`                  | yes      | Destination asset symbol.                                                        |
| `to.type`                    | yes      | `stablecoin` or `xstock`.                                                        |
| `from.amount` or `to.amount` | one of   | Set exactly one. The server calculates the other side.                           |

The only supported `network` value is `ethereum`.

## Authentication setup

Authenticated endpoints require an HMAC-SHA512 request signature in the `API-Sign` header and a monotonically increasing nonce in the `API-Nonce` header. The helper below derives the signature from the URL path, request body, and nonce. See the [Authentication guide](/tabs/developer-documentation/get-started/authentication) for the full algorithm.

<CodeGroup>
  ```python Python theme={null}
  import os
  import json
  import time
  import uuid
  import hashlib
  import hmac
  import base64
  import urllib.parse
  import requests

  API_KEY = os.environ["PWS_API_KEY"]
  API_SECRET = os.environ["PWS_API_SECRET"]
  BASE_URL = "https://api.services.payward.com"


  def get_payward_signature(urlpath, data, secret, nonce, params=None):
      encoded = (
          str(nonce).encode("utf-8")
          if data is None
          else (str(nonce) + json.dumps(data)).encode("utf-8")
      )

      sign_path = urlpath
      if params:
          sign_path += "?" + urllib.parse.urlencode(params)

      message = sign_path.encode() + hashlib.sha256(encoded).digest()
      mac = hmac.new(base64.b64decode(secret), message, hashlib.sha512)
      return base64.b64encode(mac.digest()).decode()


  def pws_headers(signature, nonce, idempotent=False):
      headers = {
          "API-Key": API_KEY,
          "API-Sign": signature,
          "API-Nonce": str(nonce),
          "Content-Type": "application/json",
      }
      if idempotent:
          headers["Idempotency-Key"] = str(uuid.uuid4())
      return headers
  ```

  ```javascript Javascript theme={null}
  import crypto from 'crypto';

  const API_KEY = process.env.PWS_API_KEY;
  const API_SECRET = process.env.PWS_API_SECRET;
  const BASE_URL = 'https://api.services.payward.com';

  function getPaywardSignature(urlpath, data, secret, nonce, params = null) {
    const encoded = data === null ? String(nonce) : String(nonce) + JSON.stringify(data);

    let signPath = urlpath;
    if (params) {
      signPath += '?' + new URLSearchParams(params).toString();
    }

    const sha256Hash = crypto.createHash('sha256').update(encoded).digest();
    const message = Buffer.concat([Buffer.from(signPath), sha256Hash]);

    const secretBuffer = Buffer.from(secret, 'base64');
    const hmac = crypto.createHmac('sha512', secretBuffer);
    hmac.update(message);
    return hmac.digest('base64');
  }

  function pwsHeaders(signature, nonce, { idempotent = false } = {}) {
    const headers = {
      'API-Key': API_KEY,
      'API-Sign': signature,
      'API-Nonce': String(nonce),
      'Content-Type': 'application/json',
    };
    if (idempotent) {
      headers['Idempotency-Key'] = crypto.randomUUID();
    }
    return headers;
  }
  ```
</CodeGroup>

## Step 1: create an on-chain quote

Send a `POST` to `/v1/accounts/{account_id}/onchain-quotes` with `from.wallet` populated. The create response returns the full quote including `execution_details`, which carries the proxy contract address and the signed on-chain payload.

<Warning>
  Store `execution_details` from the create response. `GET /v1/accounts/{account_id}/onchain-quotes/{quote_id}` returns
  quote status and amounts, but it does not return a fresh on-chain execution payload.
</Warning>

## Step 2: submit the on-chain transaction

Call `executeSwap` on `execution_details.proxy_address`. Pass the returned `proxy_swap_message` before the quote expires.

The proxy call uses this argument shape:

```typescript Typescript theme={null}
const proxyAbi = [
  {
    type: 'function',
    name: 'executeSwap',
    stateMutability: 'nonpayable',
    outputs: [],
    inputs: [
      {
        name: 'proxySwapMessage',
        type: 'tuple',
        components: [
          {
            name: 'atomicMessage',
            type: 'tuple',
            components: [
              { name: 'quoteId', type: 'bytes32' },
              { name: 'expiration', type: 'uint256' },
              {
                name: 'incomingTransfer',
                type: 'tuple',
                components: [
                  { name: 'from', type: 'address' },
                  { name: 'to', type: 'address' },
                  { name: 'token', type: 'address' },
                  { name: 'amount', type: 'uint256' },
                ],
              },
              {
                name: 'outgoingTransfer',
                type: 'tuple',
                components: [
                  { name: 'from', type: 'address' },
                  { name: 'to', type: 'address' },
                  { name: 'token', type: 'address' },
                  { name: 'amount', type: 'uint256' },
                ],
              },
            ],
          },
          { name: 'atomicSignature', type: 'bytes' },
          {
            name: 'inputTokenPermit',
            type: 'tuple',
            components: [
              { name: 'token', type: 'address' },
              { name: 'value', type: 'uint256' },
              { name: 'v', type: 'uint8' },
              { name: 'r', type: 'bytes32' },
              { name: 's', type: 'bytes32' },
            ],
          },
          { name: 'signature', type: 'bytes' },
        ],
      },
    ],
  },
] as const;

const zeroPermit = {
  token: '0x0000000000000000000000000000000000000000',
  value: 0n,
  v: 0,
  r: '0x0000000000000000000000000000000000000000000000000000000000000000',
  s: '0x0000000000000000000000000000000000000000000000000000000000000000',
};

function tokenTransfer(transfer) {
  return {
    from: transfer.from,
    to: transfer.to,
    token: transfer.token,
    amount: BigInt(transfer.amount),
  };
}

const execution = quote.data.execution_details;
const message = execution.proxy_swap_message;
const atomic = message.atomic_message;

const proxyMessage = {
  atomicMessage: {
    quoteId: atomic.quote_id,
    expiration: BigInt(atomic.expiration),
    incomingTransfer: tokenTransfer(atomic.incoming_transfer),
    outgoingTransfer: tokenTransfer(atomic.outgoing_transfer),
  },
  atomicSignature: message.atomic_signature,
  inputTokenPermit: message.input_token_permit ?? zeroPermit,
  signature: message.signature,
};

await walletClient.writeContract({
  address: execution.proxy_address,
  abi: proxyAbi,
  functionName: 'executeSwap',
  args: [proxyMessage],
});
```

If you do not pass an input-token permit, approve the proxy to spend the source token from the user's wallet before you broadcast the transaction.

<Warning>
  Onchain quote settlement happens on-chain. There is no server-side execute endpoint — the proxy contract call is the
  only way to fulfil an on-chain quote offer.
</Warning>

## Step 3: poll for terminal status

After broadcasting on-chain, poll `GET /v1/accounts/{account_id}/onchain-quotes/{quote_id}` until `status` reaches a terminal value (`executed`, `expired`, or `failed`).

<Tip>Subscribe to the `quote.executed` and `quote.execution_failed` webhooks if you do not want to poll.</Tip>

## Reviewing on-chain quotes

List previously created on-chain quotes with `GET /v1/accounts/{account_id}/onchain-quotes`. Results are returned newest-first. Use `page_token` and `page_size` (defaults to 50, max 200) for pagination — `next_page_token` is omitted on the final page.

<Note>
  History entries omit `execution_details`. The signed on-chain payload is only returned by the create response.
</Note>

## Error handling

| HTTP status               | Code                     | Cause                                                        | Remediation                                                           |
| ------------------------- | ------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------- |
| `400 Bad Request`         | validation error         | `from.wallet.address` is not an approved same-owner address. | Have the user add the wallet in the Payward UI and wait for approval. |
| `400 Bad Request`         | validation error         | Both or neither of `from.amount` / `to.amount` were set.     | Set exactly one side of the trade.                                    |
| `400 Bad Request`         | validation error         | `from`/`to` pair is not an xStock/USDC trade.                | Restrict trades to xStock ↔ stablecoin pairs.                         |
| `403 Forbidden`           | forbidden                | The user is not eligible for on-chain quote trading.         | Confirm the user's eligibility with your account manager.             |
| `410 Gone`                | `quote_expired`          | `expires_at` elapsed before settlement.                      | Request a fresh quote and broadcast more quickly.                     |
| `503 Service Unavailable` | `insufficient_liquidity` | No liquidity is available at the requested size.             | Retry with a smaller size. If it persists, contact support.           |

## Best practices

1. Register wallets ahead of time. Screening can take time and will block quote creation until the address is approved.
2. Store the on-chain quote create response. It is the only response that includes the signed on-chain execution payload.
3. Treat `executed`, `expired`, and `failed` as terminal. Request a new quote for another attempt.
4. Submit on-chain well before `expires_at`. Gas spikes and confirmation time reduce the usable window.
5. Capture `Request-Id` from response headers. It speeds up support investigations.

## API reference

| Endpoint                                              | Method | Description                                                                        |
| ----------------------------------------------------- | ------ | ---------------------------------------------------------------------------------- |
| `/v1/accounts/{account_id}/onchain-quotes`            | POST   | Create a wallet-funded on-chain quote. Returns the quote with `execution_details`. |
| `/v1/accounts/{account_id}/onchain-quotes`            | GET    | List on-chain quotes for the authenticated user (paginated, newest first).         |
| `/v1/accounts/{account_id}/onchain-quotes/{quote_id}` | GET    | Read on-chain quote status and amounts. Does not return a new execution payload.   |
