You’ll need: a signed-in user (Authentication & sessions) with an
EVM wallet linked to the account, and client code that can ask that
wallet to sign.
Result: a fill signed by the user’s wallet, polled to a terminal state.
GET /orderbook/{venueMarketOutcomeId}/route, then
POST /execution/fill. signingAddress is the only opt-in. There is no app flag or header.
The flow
1
Quote with signingAddress
Prices the route against that wallet’s balances. Read
custodyWarnings before the user
commits: the quote still succeeds, but the fill will not.2
Fill with the same address
Post the
quoteId and the same signingAddress. The response returns before any signing
happens, so it never carries pendingSignatures.3
Sign what the trade asks for
Poll
GET /execution/status?quoteId= for pendingSignatures[], sign each payload, and submit
them to POST /execution/fill/{quoteId}/signatures. Repeat until terminal is true. An empty
list means “keep polling”, not “done”.fillSelfCustody in @agg-build/sdk does steps 2 and 3 for you. Pass a signer to
createAggClient, and it posts the fill, polls, calls your signer for each request, and submits the
results. It returns once the fill is filled or partially_filled, and throws if the fill ends
failed, cancelled, or expired, if a round of signing did not move it forward, or after five
minutes. You can also drive the loop yourself.
The user has to stay present: every request expires, and an expired request cannot be revived. The
trade has to be quoted again from the start. See
Timeouts.
How to write the signer, what each request type asks for, and how many prompts to expect are in the
Self-custody signing reference.
The signing wallet
signingAddress must match a wallet already linked to the signed-in account through
POST /users/me/link-account/start and POST /users/me/link-account/confirm. Sign the message
from the start response verbatim; the confirm step compares it byte for byte.
Solana wallets do not qualify. A wallet linked with a Solana signature cannot sign for the venues
self-custody supports.
Two refusals appear here, both 400 with a message and no code:
Funding from more than one wallet
By default a trade is funded fromsigningAddress alone. fundingAddresses adds other wallets the
same user has linked, so one trade can draw on several balances.
The signing wallet still signs the venue order. Each funding wallet signs only the transfers that
move its own money, so signerAddress on a request varies within one fill. Route each request
to the wallet it names.
- Optional. Omitting it keeps the default.
- Repeat the key on the quote query (
?fundingAddresses=0x…&fundingAddresses=0x…); send an array in the fill body. - Every entry must be linked to the same account. Solana entries are refused.
- Duplicates and
signingAddressitself are removed, and order does not matter: AGG decides which wallet pays what. The quote echoes the final set back asfundingAddresses. - On the venue’s own chain only the signing wallet’s balance is usable. A funding wallet’s Hyperliquid balance cannot pay for a Hyperliquid trade, but it can move to other venues.
- Self-custody limit buys accept the same field, checked when the order is placed.
fundingAddresses on the fill it must match the quote’s set, or the fill is refused
with quote_unfillable. Leaving it off the fill skips that check.
Quote and fill
getSmartRoute() does not send signingAddress or fundingAddresses, so use client.request
for the quote. Configure your authenticated client with the signer from the
signing reference, then call
fillSelfCustody to execute and answer the signing requests. The cURL flow requires the
manual signing loop after the fill.
signingAddress on the fill must match the quoted wallet. Paper mode does not support it.
Referral fields are on Fees. Omit approveMode, or send
approveMode: "sponsored", unless you need the other approval.
custodyWarnings is absent when there is nothing to report, and is only built on buy quotes.
Never read its absence as “this will fill”. A sell uses sellShares instead of maxSpend, not both.
Fees
App and referral fees work as on a managed trade (Fees), but they are collected differently:
The Polymarket buy pre-sign is optional: its request carries
purpose: "app_fee" and
optional: true, and the order never waits for it. The SDK submits the order signatures first,
then allows up to 55 seconds for the fee signature. Declining it, or not answering, keeps the
order; about a minute after the fill the request expires and one exact fee signature is requested
after the fill instead. Sell fee pre-signs remain required.
On a HyperCore-funded Hyperliquid trade, the fee asks for up to three extra signatures (your fee, the
platform fee, and a user-paid referral), parked next to the order signature. Only non-zero legs are
asked for, and the trade is not submitted until all are returned.
Self-custody sell fees are available on Polymarket and Hyperliquid only, and only when the platform
has enabled them. Otherwise the sell is quoted fee-free with a self_custody_app_fee warning (in warnings[], not custodyWarnings) and is
never refused. Polymarket proxy wallets skip the pre-sign and use only the exact post-fill
signature.
An after-fill fee request arrives once the order already reads filled. Until it is answered, the
fill stays at overallState: "confirming" with terminal: false, so keep polling and prompting
until terminal is true. The request expires after 30 minutes. Declining it never changes the
trade, but the fee is not collected.
A fee-bearing self-custody buy is refused when its fee cannot be collected, with
quote_self_custody_app_fee and a reason in the message (see
Not supported). A fee of 0 bips is always fine. A batch that
would only collect a very small fee is skipped, and the trade still fills.
For an app-paid referral, your backend answers one more request after the fill. Its signerAddress
is your payer wallet, not the trader’s. A client that prompts the connected wallet for every
pending request must skip requests whose signerAddress is not its own.
Venue and funding support
The venue check runs at fill time, not quote time.
Funding is read from the user’s linked wallet across the EVM chains AGG routes on, their Hyperliquid
balance, their Polymarket deposit wallet on Polygon, and a legacy Polymarket proxy wallet where one
exists. Solana balances are not visible to a self-custody quote.
Choosing an approve mode
Cross-chain funding needs a token approval. There are two ways to give it. It cannot change once the fill is running.- Omit the field, or send
approveMode: "sponsored". The user does not send an approve transaction. The wallet signs aneip7702_authorization: a signature over a raw digest, neither EIP-191 nor EIP-712. Many wallets cannot produce it. Where it works, the user signs once per chain, and later bridges on that chain need no further approve signature. approveMode: "user_broadcast": the user sends an ordinary approve astype: "transaction"and pays that transaction’s network fee. It sets up no delegation, so every bridge needs an approve.
approveMode: "user_broadcast" when the wallet cannot sign a raw-digest authorization. A permit, when the route uses one, stays eip712. That is not the EIP-7702 step. The same split is on Self-custody signing.
Neither mode makes bridging free. The bridge provider’s fee is taken from the amount delivered on
every bridge, and one-time costs such as Hyperliquid’s first-deposit activation apply per route.
Quote with deepEstimate=true and read feeBreakdown.bridgeFees and feeBreakdown.setupCosts. See
Deep cost estimate.
Three refusals here are terminal and show as errorReason on the status endpoint, not as a 400
on the fill: a wallet already delegated to a different contract on the source chain, a
approveMode: "user_broadcast" wallet with no native fee to send the approve, and a same-chain conversion under
approveMode: "user_broadcast", which needs approveMode: "sponsored" (or the field omitted).
Not supported, with the exact code
Every refusal below is400 on POST /execution/fill with { message, code }.
Refusals with no
code, only a message:
signingAddress is not supported in paper mode.- The two wallet-linking messages above.
code misses these. Always show message too.
These paths do not accept a signing wallet:
- Direct orders (
POST /execution/orders) rejectsigningAddressat validation. - Withdrawals always act on the managed wallet. A self-custody user already holds their funds.
Self-custody limit orders
Pass the linkedsigningAddress to POST /execution/limit-orders for Polymarket or Hyperliquid.
Use the returned quoteId to poll GET /execution/status, sign each pendingSignatures batch, and
submit it to POST /execution/fill/{quoteId}/signatures. An empty batch can mean the next step is
still running. Placement succeeds when the order is open with a venueOrderId. A resting order
is not terminal: track it with GET /execution/orders?orderId=.
Polymarket asks for fresh authentication for each new order. The credentials derived from it are
used in memory during submission and are not stored.
AGG tracks fills in the background. An empty fill history does not prove an order was cancelled or
expired, and an unfilled remainder that the venue cancelled or expired can show as active until a
fresh signed read confirms it.
Cancel with POST /execution/orders/cancel and { orderId, cancelSignature }. AGG cannot cancel a
self-custody order without a new signature from the wallet. GTD expiry is enforced by the venue.
AGG does not cancel self-custody GTC orders when a market closes.
Related
Self-custody signing reference
Request types, the signer, prompt counts, and the raw signing loop.
Execute quote
Request and response schema for
POST /execution/fill.Submit signatures
The endpoint behind the signing loop.
Account linking
Link the EVM wallet self-custody needs.