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

> ## Agent Instructions
> To integrate AGG, start with Quickstart: REST (https://docs.agg.market/quickstart/rest), then Order lifecycle & statuses (https://docs.agg.market/concepts/order-lifecycle).
> Track every trade until it reaches a terminal status. Before retrying a failed or timed-out call, read Errors, retries & idempotency (https://docs.agg.market/concepts/errors).
> The API reference is generated from https://docs.agg.market/openapi/openapi.json.

# Self-Custody Signing Reference

> Signature request types, a complete signer, how many prompts a user sees, and the raw signing loop

This page is the reference for the signing half of [Self-custody trading](/recipes/self-custody):
what each request asks the wallet to do, how to answer it, and what errors mean.

## The request

Each entry in `pendingSignatures[]` looks like this:

```ts theme={null}
{
  stepId: string;        // send it back verbatim
  type: SignatureRequestType;
  venue: string;         // who the request is for; switch on `type`, not this
  signerAddress: string; // the wallet that must sign or send
  chainId?: number;      // often absent; read the chain from the payload
  payload: unknown;      // sign or send it verbatim
  expiresAt: string;     // ISO-8601; always read it
  purpose?: string;      // for display: funding, funding_and_fee, fee, app_fee, referral_payout
}
```

## What each type asks for

| `type` | What to call | Submit |
| - | - | - |
| `eip712` | `signTypedData(payload)` | `signature` |
| `hl_l1_action` | `signTypedData(payload)`, EIP-712 for a Hyperliquid action | `signature` |
| `personal_sign` | `signMessage({ message: payload })` when `payload` is a string, or `signMessage({ message: { raw } })` when it is `{ raw }` | `signature` |
| `eip7702_authorization` | `signAuthorization(payload)` over `{ contractAddress, chainId, nonce }`, then serialize to a 65-byte signature | `signature` |
| `safe_tx` | `signMessage({ message: { raw: payload.safeTxHash } })`, a `personal_sign` over the raw 32-byte SafeTx hash. Used for a legacy Polymarket Safe. | `signature` |
| `transaction` | Switch the wallet to `payload.chainId`, then send `{ to, value, data }` from `signerAddress` | `txHash` |
| `solana_transaction` | Reserved. Not issued. | |

Send exactly one of `signature` and `txHash` per step. Signatures are `0x`-prefixed hex.

A fill funded entirely from a legacy Polymarket Safe asks for one `safe_tx` signature and never an
approve or an EIP-7702 authorization. A fill funded from a Safe plus another wallet on the same
chain also asks for the usual steps for that other wallet.

## A complete signer

`fillSelfCustody` calls your signer once per request. Return a hex signature, or `{ txHash }` for a
`transaction`.

```ts theme={null}
import { serializeSignature } from "viem";
import type { SignerFn } from "@agg-build/sdk";

const mySigner: SignerFn = async (req) => {
  switch (req.type) {
    case "eip712":
    case "hl_l1_action":
      return walletClient.signTypedData(req.payload as never);

    case "personal_sign": {
      const p = req.payload as string | { raw: `0x${string}` };
      return walletClient.signMessage({ message: typeof p === "string" ? p : { raw: p.raw } });
    }

    case "safe_tx": {
      const { safeTxHash } = req.payload as { safeTxHash: `0x${string}` };
      return walletClient.signMessage({ message: { raw: safeTxHash } });
    }

    case "eip7702_authorization": {
      const auth = await walletClient.signAuthorization(req.payload as never);
      // viem types `yParity` and `v` as optional. Derive rather than default:
      // a wrong parity recovers to another address and the server rejects it.
      const yParity = auth.yParity ?? (auth.v === undefined ? undefined : Number(auth.v) - 27);
      if (yParity !== 0 && yParity !== 1) throw new Error("authorization has no usable parity");
      return serializeSignature({ r: auth.r, s: auth.s, yParity });
    }

    case "transaction": {
      const tx = req.payload as { chainId: string; to: string; value: string; data: string };
      // Broadcast on the request's chain, not whichever one the wallet is on.
      await walletClient.switchChain({ id: Number(tx.chainId) });
      // Return the hash right away. Do NOT wait for the receipt.
      return {
        txHash: await walletClient.sendTransaction({
          to: tx.to as `0x${string}`,
          value: BigInt(tx.value), // decimal wei string; "0" for an approve
          data: tx.data as `0x${string}`,
        }),
      };
    }

    default:
      throw new Error(`unsupported signature request: ${req.type}`);
  }
};
```

<Warning>
  **Throwing from your signer is not a rejection.** `fillSelfCustody` stops at once and drops the
  signatures it had collected for that batch, but the server has no reject call: the request
  simply expires and the fill fails. If you know a wallet cannot produce a raw-digest EIP-7702
  authorization, fill with `approveMode: "user_broadcast"` instead.
</Warning>

**When `transaction` reaches the trader.** Only when the fill has to bridge, the source token needs
an on-chain approve rather than a permit signature, **and** the fill set
`approveMode: "user_broadcast"`. In the default mode the same approval is signed as `eip712`.
Venue orders, the Polymarket wrap, and the Hyperliquid builder-fee approval are always signatures.

The other `transaction` you may see is an app-paid referral payout addressed to your payer wallet.
Check `signerAddress` before you prompt. See [Fees](/concepts/fees#app-paid).

After a `transaction`, return the hash without waiting. AGG watches for the mined receipt and does
not continue until it matches the request.

## Timeouts, and what expiry means

Every request carries `expiresAt`. **Read it; never hard-code a deadline.** Steps in one fill can
have different deadlines, and the shortest is tight:

| Request | Deadline |
| - | - |
| Hyperliquid order | 2 minutes |
| Hyperliquid builder-fee approval | 90 seconds |
| Polymarket order pair | 90 seconds |
| Bridge signatures | 2 minutes |
| Polymarket setup | 5 minutes |
| Polymarket fund transfer | 2 minutes |
| `transaction` | 15 minutes |

When a fill dies, `GET /execution/status` reports `overallState` `failed` or `expired` with an
`errorReason`, and `fillSelfCustody` throws with that state in its message.

An expired request disappears from the pending list rather than showing as expired, so an empty list
never means "done". Only `terminal: true` does. Submitting a signature for an expired request
returns `400`. There is no recovery: quote again. A user has one live execution at a time. If a
request expires instead of being answered, a new fill does not start until the abandoned one has
finished failing, so ask the user to complete or abandon deliberately.

## How many wallet prompts the user sees

Requests parked together arrive in one batch, but the user approves each one. The total is the sum
of three parts: the venue order, a Polymarket funding step, and the funding transfers.

**Venue order**

| Situation | Prompts |
| - | - |
| Hyperliquid order | 1 |
| Hyperliquid builder-fee approval | 1, until granted for that wallet. Apps with their own builder config never see it. |
| Polymarket order | 2, always a pair, buy or sell |

**Polymarket funding step**

| Situation | Prompts |
| - | - |
| Buy with USDC.e waiting in the deposit wallet and not enough settled pUSD | **1, after every bridge** |
| Buy already covered by settled pUSD in the deposit wallet | 0 |
| First trade on a new deposit wallet, buy or sell | 1. Sets up approvals; a buy's wrap rides in the same signature. |
| Any later sell | 0 |

<Warning>
  The wrap prompt is **not** one-time. Funding arrives as USDC.e, which must be wrapped before a
  buy settles, so a returning user sees it on every freshly funded buy.
</Warning>

**Funding transfers, per funding source.** A route funded from two chains pays this twice.

| Situation | Prompts |
| - | - |
| Hyperliquid order paid from a Hyperliquid balance | 0 |
| Polymarket order paid from USDC.e or pUSD already in the deposit wallet | 0 |
| Polymarket order paid from Polygon funds on the user's own address | 1 in the default mode, 2 if the wallet has not signed an EIP-7702 authorization yet. Refused under `user_broadcast`. |
| Cross-chain from native USDC (signature only) | 1 in either mode. With a partner fee or user-paid referral it becomes approve plus deposit so the fee rides that batch: 1, or 2 before the first authorization. |
| Cross-chain from a token needing an approval, default mode, authorization already signed | 2 |
| Cross-chain from a token needing an approval, default mode, first time on that chain | 3 |
| Cross-chain from a token needing an approval, `user_broadcast` | 2: one transaction, one signature, on **every** bridge |
| Paid from a Hyperliquid balance for another venue | 2 |

With a partner fee on an EVM-funded buy, the funding batch also carries the fee transfers. A
user-paid referral is one more transfer in the same batch.

Worked examples: a Hyperliquid buy from an existing Hyperliquid balance is **1** prompt (2 while the
builder approval is outstanding). A Polymarket buy bridged from native USDC is **4** (one funding
transfer, one wrap, two order signatures), and stays 4 on repeat. The same buy from a token needing
an approval, before the first authorization, is **6**. A Polymarket sell is **2**, or **3** the
first time that deposit wallet trades.

## Driving the loop yourself

Without `fillSelfCustody`, run this loop:

1. `POST /execution/fill` with `quoteId` and `signingAddress`. The response never carries
   `pendingSignatures`, even for self-custody.
2. Poll `GET /execution/status?quoteId=`, waiting `pollAfterMs` between calls.
3. For each new `pendingSignatures[]` entry, have the wallet named by `signerAddress` sign or send
   the payload.
4. Submit the results to `POST /execution/fill/{quoteId}/signatures` as
   `{ "signatures": [{ "stepId": "...", "signature": "0x..." }] }` (or `txHash` for a
   `transaction`). Up to 32 entries per call. The response has the same shape as the fill
   response.
5. Repeat from step 2 until `terminal` is `true`.

Errors from the signatures endpoint:

| Status | Message | Meaning |
| - | - | - |
| `403` | `no such fill for this account` | Unknown quote, or not this user's. The two look the same on purpose. |
| `404` | `no pending request for step …` | The `stepId` does not match a waiting request. |
| `400` | `request for step … has expired — re-quote and retry` | The request is dead. Start over. |
| `400` | `signature for step … is not 0x-prefixed hex` | Malformed signature. |
| `400` | `txHash for step … is not a 0x-prefixed 32-byte hash` | Malformed transaction hash. |
| `400` | `signature was signed by 0x…, expected 0x…` | The wrong key signed. Both addresses are lowercased. |
| `400` | ``step … is a signature request — return `signature`, not `txHash` `` | Wrong field for the step's type. The reverse message names a transaction step. |

Resubmitting a step that already succeeded is ignored, so retrying a batch after a network error is
safe. Do not send the same `stepId` twice in one body: duplicates collapse to the last entry, so one
bad entry discards the good one and the request fails.
