How Collateral Return Works
Collateral return finds compatible Combo positions that offset one another. It decomposes those positions, merges complementary exposure back into pUSD, and leaves the remaining exposure in residual positions. For example, letY(A) and N(A) represent the YES and NO positions for market
A, and let A ^ B ^ C represent a three-leg Combo. Suppose the wallet holds:
N(A ^ B ^ C)
N(N(A) ^ B ^ C)
N(A) + Y(A ^ N(B ^ C))
Y(A) + Y(N(A) ^ N(B ^ C))
Y(A) and N(A) are complementary, so they merge into 1 pUSD per matched
share. The two Y(…) positions remain in the wallet, preserving its unmatched
exposure to the other legs.
Return Collateral
The workflow is plan-based: inspect the proposed position changes, execute that exact plan, wait for confirmation, and request another plan when more work remains. You need a Relayer API key. See Connect Your Account to create one before continuing.- TypeScript
- Python
- API
Given a
SecureClient configured with a Relayer API key, ensure the wallet has
the required trading approvals:import { createSecureClient, relayerApiKey } from "@polymarket/client";
import { privateKey } from "@polymarket/client/viem";
const client = await createSecureClient({
wallet: process.env.POLYMARKET_WALLET_ADDRESS,
signer: privateKey(process.env.SIGNER_PRIVATE_KEY),
apiKey: relayerApiKey({
key: process.env.RELAYER_API_KEY!,
address: process.env.RELAYER_API_KEY_ADDRESS!,
}),
});
await client.setupTradingApprovals();
Collateral return supports Deposit Wallet, Safe Wallet, and Proxy Wallet
accounts. EOA accounts are not supported.
1
Request a Plan
First, request a plan based on the wallet’s current Combo positions.
import type { CollateralReturnPlanResponse } from "@polymarket/client";
const plan: CollateralReturnPlanResponse = await client.planCollateralReturn();
CollateralReturnPlanResponse
CollateralReturnPlanResponse
enum CollateralReturnKnownOperationKind {
Split = "split",
Merge = "merge",
Redeem = "redeem",
SplitOnCondition = "split_on_condition",
MergeOnCondition = "merge_on_condition",
SplitOnEvent = "split_on_event",
MergeOnEvent = "merge_on_event",
ConvertOnEvent = "convert_on_event",
Extract = "extract",
Inject = "inject",
ConvertToYesBasket = "convert_to_yes_basket",
MergeFromYesBasket = "merge_from_yes_basket",
Compress = "compress",
}
type CollateralReturnOperationKind =
| CollateralReturnKnownOperationKind
| (string & {});
type CollateralReturnOperation = {
kind: CollateralReturnOperationKind;
conditionId?: ComboConditionId;
eventId?: EventId;
positionId?: PositionId;
conditionIndex: number;
amount: DecimalString;
};
type CollateralReturnPositionAmount = {
positionId: PositionId;
amount: DecimalString;
};
type CollateralReturnPositionSummary = {
consumed: CollateralReturnPositionAmount[];
created: CollateralReturnPositionAmount[];
};
type CollateralReturnRouterCall = {
to: EvmAddress;
data: HexString;
};
type CollateralReturnPlanResponse = {
planHash: HexString;
chainId: number;
wallet: EvmAddress;
blockNumber: bigint;
startingPusd: DecimalString;
netPusdOut: DecimalString;
finalPusd: DecimalString;
operations: CollateralReturnOperation[];
operationCount: number;
truncated: boolean;
estimatedCost: number;
requiredPusdInput: DecimalString;
requiredPositions: CollateralReturnPositionAmount[];
positionSummary: CollateralReturnPositionSummary;
candidatePositionIds: PositionId[];
routerCall: CollateralReturnRouterCall;
};
{
"planHash": "<plan_hash>",
"chainId": 137,
"wallet": "<wallet_address>",
"blockNumber": "74281963",
"startingPusd": "5.000000",
"netPusdOut": "1.000000",
"finalPusd": "6.000000",
"operations": [
{
"kind": "merge_on_condition",
"conditionId": "<condition_id>",
"positionId": "<position_id>",
"conditionIndex": 0,
"amount": "1.000000"
}
],
"operationCount": 1,
"truncated": false,
"estimatedCost": 240000,
"requiredPusdInput": "0.000000",
"requiredPositions": [
{
"positionId": "<position_id>",
"amount": "1.000000"
}
],
"positionSummary": {
"consumed": [
{
"positionId": "<position_id>",
"amount": "1.000000"
}
],
"created": [
{
"positionId": "<residual_position_id>",
"amount": "0.500000"
}
]
},
"candidatePositionIds": [
"<candidate_position_id_1>",
"<candidate_position_id_2>"
],
"routerCall": {
"to": "<contract_address>",
"data": "<calldata>"
}
}
2
Inspect the Plan
Next, inspect the plan before signing. It is an inspectable execution artifact,
not just a transaction payload. Review its expected return and residual-position
impact, and apply any application-specific limits.
| Field | What to inspect |
|---|---|
startingPusd, netPusdOut, finalPusd | The expected pUSD return and resulting wallet balance. |
requiredPositions | The Combo positions the plan requires. |
operations | The ordered position changes. |
positionSummary | The positions consumed and the residual positions created. |
estimatedCost | The estimated execution cost against any application-specific limit. |
truncated | Whether more work remains after this plan confirms. |
routerCall | The exact action that will be submitted. |
3
Execute the Inspected Plan
Finally, execute the inspected plan and wait for the transaction to confirm.
const handle = await client.executeCollateralReturnPlan({ plan });
const outcome = await handle.wait();
// outcome.transactionHash: TxHash
await handle.wait() returns after the submitted plan’s transaction confirms.
Request another plan only then, because the next plan is calculated from the
resulting onchain state.If execution fails, discard the plan and request a new one before retrying.
Given an The sync
AsyncSecureClient configured with a Relayer API key, ensure the wallet
has the required trading approvals:import os
from polymarket import AsyncSecureClient, RelayerApiKey
client = await AsyncSecureClient.create(
private_key=os.environ["SIGNER_PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
api_key=RelayerApiKey(
key=os.environ["POLYMARKET_RELAYER_API_KEY"],
address=os.environ["POLYMARKET_RELAYER_API_KEY_ADDRESS"],
),
)
await client.setup_trading_approvals()
SecureClient provides the same methods.Collateral return supports Deposit Wallet, Safe Wallet, and Proxy Wallet
accounts. EOA accounts are not supported.
1
Request a Plan
First, request a plan based on the wallet’s current Combo positions.
from polymarket import CollateralReturnPlanResponse
plan: CollateralReturnPlanResponse = await client.plan_collateral_return()
CollateralReturnPlanResponse
CollateralReturnPlanResponse
class CollateralReturnOperationKind(StrEnum):
SPLIT = "split"
MERGE = "merge"
REDEEM = "redeem"
SPLIT_ON_CONDITION = "split_on_condition"
MERGE_ON_CONDITION = "merge_on_condition"
SPLIT_ON_EVENT = "split_on_event"
MERGE_ON_EVENT = "merge_on_event"
CONVERT_ON_EVENT = "convert_on_event"
EXTRACT = "extract"
INJECT = "inject"
CONVERT_TO_YES_BASKET = "convert_to_yes_basket"
MERGE_FROM_YES_BASKET = "merge_from_yes_basket"
COMPRESS = "compress"
class CollateralReturnOperation:
kind: CollateralReturnOperationKind | str
condition_id: ComboConditionId | None
event_id: EventId | None
position_id: PositionId | None
condition_index: int
amount: Decimal
class CollateralReturnPositionAmount:
position_id: PositionId
amount: Decimal
class CollateralReturnPositionSummary:
consumed: tuple[CollateralReturnPositionAmount, ...]
created: tuple[CollateralReturnPositionAmount, ...]
class CollateralReturnRouterCall:
to: EvmAddress
data: HexString
class CollateralReturnPlanResponse:
plan_hash: HexString
chain_id: int
wallet: EvmAddress
block_number: int
starting_pusd: Decimal
net_pusd_out: Decimal
final_pusd: Decimal
operations: tuple[CollateralReturnOperation, ...]
operation_count: int
truncated: bool
estimated_cost: float
required_pusd_input: Decimal
required_positions: tuple[CollateralReturnPositionAmount, ...]
position_summary: CollateralReturnPositionSummary
candidate_position_ids: tuple[PositionId, ...]
router_call: CollateralReturnRouterCall
{
"plan_hash": "<plan_hash>",
"chain_id": 137,
"wallet": "<wallet_address>",
"block_number": 74281963,
"starting_pusd": "5.000000",
"net_pusd_out": "1.000000",
"final_pusd": "6.000000",
"operations": [
{
"kind": "merge_on_condition",
"condition_id": "<condition_id>",
"position_id": "<position_id>",
"condition_index": 0,
"amount": "1.000000"
}
],
"operation_count": 1,
"truncated": false,
"estimated_cost": 240000,
"required_pusd_input": "0.000000",
"required_positions": [
{
"position_id": "<position_id>",
"amount": "1.000000"
}
],
"position_summary": {
"consumed": [
{
"position_id": "<position_id>",
"amount": "1.000000"
}
],
"created": [
{
"position_id": "<residual_position_id>",
"amount": "0.500000"
}
]
},
"candidate_position_ids": [
"<candidate_position_id_1>",
"<candidate_position_id_2>"
],
"router_call": {
"to": "<contract_address>",
"data": "<calldata>"
}
}
2
Inspect the Plan
Next, inspect the plan before signing. Review its expected return and
residual-position impact, and apply any application-specific limits.
| Field | What to inspect |
|---|---|
starting_pusd, net_pusd_out, final_pusd | The expected pUSD return and resulting wallet balance. |
required_positions | The Combo positions the plan requires. |
operations | The ordered position changes. |
position_summary | The positions consumed and the residual positions created. |
estimated_cost | The estimated execution cost against any application-specific limit. |
truncated | Whether more work remains after this plan confirms. |
router_call | The exact action that will be submitted. |
3
Execute the Inspected Plan
Finally, execute the inspected plan and wait for the transaction to confirm.
handle = await client.execute_collateral_return_plan(plan=plan)
outcome = await handle.wait()
# outcome.transaction_hash: TransactionHash
await handle.wait() returns after the submitted plan’s transaction confirms.
Request another plan only then, because the next plan is calculated from the
resulting onchain state.If execution fails, discard the plan and request a new one before retrying.
The following steps show the Deposit Wallet path. For Safe or Proxy Wallet
flows, use an SDK or see the reference examples at the end of this tab.
| Asset | Contract to approve | Contract call |
|---|---|---|
| pUSD | Collateral Return Router | approve(CollateralReturnRouter, maxUint256) |
| Combo positions | Collateral Return Router | setApprovalForAll(CollateralReturnRouter, true) |
| Contract | Address |
|---|---|
| pUSD | 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB |
| Position Manager | 0x006F54F7f9A22e0000CC2AB60031000000ae9fEF |
| Collateral Return Router | 0x12121212006e4CD160D18e3f00711DA5c3372600 |
1
Request a Plan
First, request a plan for the Deposit Wallet whose Combo positions you want to
process.The plan is calculated from one pinned block and contains the position changes
and Router call for that state snapshot.
Nested
curl -X POST "https://combos-rfq-collateral-return.polymarket.com/v1/collateral-return/plan" \
-H "Content-Type: application/json" \
--data '{
"wallet": "<deposit_wallet_address>"
}'
Collateral Return Plan
Collateral Return Plan
{
"plan_hash": "<plan_hash>",
"chain_id": 137,
"wallet": "<deposit_wallet_address>",
"block_number": "74281963",
"starting_pusd": "5.000000",
"net_pusd_out": "1.000000",
"final_pusd": "6.000000",
"operations": [
{
"kind": "merge_on_condition",
"condition_id": "<condition_id>",
"position_id": "<position_id>",
"condition_index": 0,
"amount": "1000000"
}
],
"operation_count": 1,
"truncated": false,
"estimated_cost": 240000,
"required_positions": [
{
"position_id": "<position_id>",
"amount": "1000000"
}
],
"position_summary": {
"consumed": [
{
"position_id": "<consumed_position_id>",
"amount": "1000000"
}
],
"created": [
{
"position_id": "<residual_position_id>",
"amount": "1000000"
}
]
},
"candidate_position_ids": [
"<candidate_position_id_1>",
"<candidate_position_id_2>"
],
"router_call": {
"to": "<router_address>",
"data": "<calldata>"
}
}
amount values use six-decimal base units, so 1000000 represents one
position unit.2
Inspect the Plan
Next, inspect the plan before signing. Review its expected return,
residual-position impact, and execution cost against any application-specific
limits.
| Field | What to inspect |
|---|---|
starting_pusd, net_pusd_out, final_pusd | The expected pUSD return and resulting wallet balance. |
required_positions | The Combo positions the plan consumes. |
position_summary | The positions consumed and created, including residual positions. |
operations | The ordered position changes. |
estimated_cost | The estimated execution cost against any application-specific limit. |
truncated | Whether more work remains after this plan confirms. |
router_call | The exact Router action that the wallet will authorize. |
plan_hash | The correlation value to send with the signed wallet action. |
3
Fetch the Wallet Nonce
Then, fetch a fresh Deposit Wallet nonce for the signer from the Relayer.
curl -G "https://relayer-v2.polymarket.com/v1/account/transactions/params" \
-H "RELAYER_API_KEY: <relayer_api_key>" \
-H "RELAYER_API_KEY_ADDRESS: <signer_address>" \
--data-urlencode "address=<signer_address>" \
--data-urlencode "type=WALLET"
Response
{
"address": "<signer_address>",
"nonce": "<wallet_nonce>"
}
4
Create the Batch Typed Data
Next, create Deposit Wallet EIP-712
Batch typed data containing exactly one
call. Copy router_call.to and router_call.data from the plan without
modification, and use 0 for the call value. Set deadline to a future Unix
timestamp; the complete example below uses ten minutes from the current time.walletBatchTypedData
{
"domain": {
"name": "DepositWallet",
"version": "1",
"chainId": 137,
"verifyingContract": "<deposit_wallet_address>"
},
"types": {
"Call": [
{ "name": "target", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "data", "type": "bytes" }
],
"Batch": [
{ "name": "wallet", "type": "address" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" },
{ "name": "calls", "type": "Call[]" }
]
},
"primaryType": "Batch",
"message": {
"wallet": "<deposit_wallet_address>",
"nonce": "<wallet_nonce>",
"deadline": "<unix_seconds>",
"calls": [
{
"target": "<router_call.to>",
"value": "0",
"data": "<router_call.data>"
}
]
}
}
5
Sign the Batch
Then, sign the typed data with the signer that controls the Deposit Wallet. The
example below uses Viem.
import { privateKeyToAccount } from "viem/accounts";
const signer = privateKeyToAccount(
process.env.SIGNER_PRIVATE_KEY as `0x${string}`,
);
const signature = await signer.signTypedData(walletBatchTypedData);
6
Execute the Plan
Next, send the plan hash and signed Deposit Wallet envelope to the Collateral
Return API. Use the same nonce, deadline, and Router call that you signed.The Collateral Return service validates and simulates the signed action against
current state before forwarding it to the Relayer. The response includes the
transaction ID used in the next step.
curl -X POST "https://combos-rfq-collateral-return.polymarket.com/v1/collateral-return/submit" \
-H "Content-Type: application/json" \
-H "RELAYER_API_KEY: <relayer_api_key>" \
-H "RELAYER_API_KEY_ADDRESS: <signer_address>" \
--data '{
"plan_hash": "<plan_hash>",
"envelope": {
"type": "WALLET",
"from": "<signer_address>",
"to": "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07",
"nonce": "<wallet_nonce>",
"signature": "<signature>",
"metadata": "Return Combo collateral",
"depositWalletParams": {
"depositWallet": "<deposit_wallet_address>",
"deadline": "<unix_seconds>",
"calls": [
{
"target": "<router_call.to>",
"value": "0",
"data": "<router_call.data>"
}
]
}
}
}'
Response
{
"transactionID": "<transaction_id>",
"state": "STATE_NEW"
}
If submission returns
409 fresh plan required, discard the plan and request
a new one before rebuilding and signing the wallet action.7
Confirm the Transaction
Finally, poll the Relayer transaction until it reaches
STATE_CONFIRMED.curl "https://relayer-v2.polymarket.com/v1/account/transactions/<transaction_id>" \
-H "RELAYER_API_KEY: <relayer_api_key>" \
-H "RELAYER_API_KEY_ADDRESS: <signer_address>"
Response
{
"transaction_id": "<transaction_id>",
"transaction_hash": "<transaction_hash>",
"state": "STATE_CONFIRMED",
"error_msg": null
}
| State | Action |
|---|---|
STATE_NEW, STATE_EXECUTED, STATE_MINED | Continue polling. |
STATE_CONFIRMED | The transaction confirmed successfully. |
STATE_FAILED, STATE_INVALID | Discard the plan and request a new one before retrying. |
import { privateKeyToAccount } from "viem/accounts";
const COLLATERAL_RETURN_API =
"https://combos-rfq-collateral-return.polymarket.com";
const RELAYER_API = "https://relayer-v2.polymarket.com";
const DEPOSIT_WALLET_FACTORY = "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07";
const account = privateKeyToAccount(process.env.SIGNER_PRIVATE_KEY);
const wallet = process.env.POLYMARKET_WALLET_ADDRESS;
const relayerHeaders = {
RELAYER_API_KEY: process.env.RELAYER_API_KEY,
RELAYER_API_KEY_ADDRESS: account.address,
};
// Request a plan.
const planResponse = await fetch(
`${COLLATERAL_RETURN_API}/v1/collateral-return/plan`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet }),
},
);
if (!planResponse.ok) throw new Error(await planResponse.text());
const plan = await planResponse.json();
// Inspect the plan and apply any application-specific limits before signing.
console.log({
netPusdOut: plan.net_pusd_out,
positionSummary: plan.position_summary,
estimatedCost: plan.estimated_cost,
truncated: plan.truncated,
});
// Fetch a fresh Deposit Wallet nonce.
const executeParamsResponse = await fetch(
`${RELAYER_API}/v1/account/transactions/params?address=${account.address}&type=WALLET`,
{ headers: relayerHeaders },
);
if (!executeParamsResponse.ok) {
throw new Error(await executeParamsResponse.text());
}
const executeParams = await executeParamsResponse.json();
// Create and sign the Deposit Wallet batch.
const deadline = BigInt(Math.floor(Date.now() / 1_000) + 600);
const calls = [
{
target: plan.router_call.to,
value: 0n,
data: plan.router_call.data,
},
];
const signature = await account.signTypedData({
domain: {
name: "DepositWallet",
version: "1",
chainId: 137,
verifyingContract: wallet,
},
types: {
Call: [
{ name: "target", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
],
Batch: [
{ name: "wallet", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "calls", type: "Call[]" },
],
},
primaryType: "Batch",
message: {
wallet,
nonce: BigInt(executeParams.nonce),
deadline,
calls,
},
});
const envelope = {
type: "WALLET",
from: account.address,
to: DEPOSIT_WALLET_FACTORY,
nonce: executeParams.nonce,
signature,
metadata: "Return Combo collateral",
depositWalletParams: {
depositWallet: wallet,
deadline: deadline.toString(),
calls: calls.map((call) => ({
...call,
value: call.value.toString(),
})),
},
};
// Execute the plan.
const submitResponse = await fetch(
`${COLLATERAL_RETURN_API}/v1/collateral-return/submit`,
{
method: "POST",
headers: { "Content-Type": "application/json", ...relayerHeaders },
body: JSON.stringify({ plan_hash: plan.plan_hash, envelope }),
},
);
if (!submitResponse.ok) throw new Error(await submitResponse.text());
const submitted = await submitResponse.json();
// Confirm the transaction.
let outcome;
for (;;) {
const transactionResponse = await fetch(
`${RELAYER_API}/v1/account/transactions/${submitted.transactionID}`,
{ headers: relayerHeaders },
);
if (!transactionResponse.ok) {
throw new Error(await transactionResponse.text());
}
const transaction = await transactionResponse.json();
if (transaction.state === "STATE_CONFIRMED") {
outcome = transaction;
break;
}
if (
transaction.state === "STATE_FAILED" ||
transaction.state === "STATE_INVALID"
) {
throw new Error(transaction.error_msg ?? transaction.state);
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
console.log(outcome);
import { hashTypedData, zeroAddress } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const COLLATERAL_RETURN_API =
"https://combos-rfq-collateral-return.polymarket.com";
const RELAYER_API = "https://relayer-v2.polymarket.com";
const account = privateKeyToAccount(process.env.SIGNER_PRIVATE_KEY);
const wallet = process.env.POLYMARKET_WALLET_ADDRESS;
const relayerHeaders = {
RELAYER_API_KEY: process.env.RELAYER_API_KEY,
RELAYER_API_KEY_ADDRESS: account.address,
};
// Request a plan.
const planResponse = await fetch(
`${COLLATERAL_RETURN_API}/v1/collateral-return/plan`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet }),
},
);
if (!planResponse.ok) throw new Error(await planResponse.text());
const plan = await planResponse.json();
// Inspect the plan and apply any application-specific limits before signing.
console.log({
netPusdOut: plan.net_pusd_out,
positionSummary: plan.position_summary,
estimatedCost: plan.estimated_cost,
truncated: plan.truncated,
});
// Fetch a fresh Safe nonce.
const executeParamsResponse = await fetch(
`${RELAYER_API}/v1/account/transactions/params?address=${account.address}&type=SAFE`,
{ headers: relayerHeaders },
);
if (!executeParamsResponse.ok) {
throw new Error(await executeParamsResponse.text());
}
const executeParams = await executeParamsResponse.json();
// Create and sign the Safe transaction.
const message = {
to: plan.router_call.to,
value: 0n,
data: plan.router_call.data,
operation: 0,
safeTxGas: 0n,
baseGas: 0n,
gasPrice: 0n,
gasToken: zeroAddress,
refundReceiver: zeroAddress,
nonce: BigInt(executeParams.nonce),
};
const digest = hashTypedData({
domain: { chainId: 137, verifyingContract: wallet },
types: {
SafeTx: [
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
{ name: "operation", type: "uint8" },
{ name: "safeTxGas", type: "uint256" },
{ name: "baseGas", type: "uint256" },
{ name: "gasPrice", type: "uint256" },
{ name: "gasToken", type: "address" },
{ name: "refundReceiver", type: "address" },
{ name: "nonce", type: "uint256" },
],
},
primaryType: "SafeTx",
message,
});
const signed = (await account.signMessage({ message: { raw: digest } })).slice(
2,
);
const v = Number.parseInt(signed.slice(128, 130), 16);
const packedV = v === 0 || v === 1 ? v + 31 : v === 27 || v === 28 ? v + 4 : v;
const signature = `0x${signed.slice(0, 128)}${packedV.toString(16).padStart(2, "0")}`;
const envelope = {
type: "SAFE",
from: account.address,
to: message.to,
proxyWallet: wallet,
value: "0",
data: message.data,
nonce: executeParams.nonce,
signature,
metadata: "Return Combo collateral",
signatureParams: {
operation: "0",
safeTxnGas: "0",
baseGas: "0",
gasPrice: "0",
gasToken: zeroAddress,
refundReceiver: zeroAddress,
},
};
// Execute the plan.
const submitResponse = await fetch(
`${COLLATERAL_RETURN_API}/v1/collateral-return/submit`,
{
method: "POST",
headers: { "Content-Type": "application/json", ...relayerHeaders },
body: JSON.stringify({ plan_hash: plan.plan_hash, envelope }),
},
);
if (!submitResponse.ok) throw new Error(await submitResponse.text());
const submitted = await submitResponse.json();
// Confirm the transaction.
let outcome;
for (;;) {
const transactionResponse = await fetch(
`${RELAYER_API}/v1/account/transactions/${submitted.transactionID}`,
{ headers: relayerHeaders },
);
if (!transactionResponse.ok) {
throw new Error(await transactionResponse.text());
}
const transaction = await transactionResponse.json();
if (transaction.state === "STATE_CONFIRMED") {
outcome = transaction;
break;
}
if (
transaction.state === "STATE_FAILED" ||
transaction.state === "STATE_INVALID"
) {
throw new Error(transaction.error_msg ?? transaction.state);
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
console.log(outcome);
import {
concat,
createPublicClient,
encodeFunctionData,
http,
keccak256,
toHex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { polygon } from "viem/chains";
const COLLATERAL_RETURN_API =
"https://combos-rfq-collateral-return.polymarket.com";
const RELAYER_API = "https://relayer-v2.polymarket.com";
const PROXY_WALLET_FACTORY = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
const RELAY_HUB = "0xD216153c06E857cD7f72665E0aF1d7D82172F494";
const account = privateKeyToAccount(process.env.SIGNER_PRIVATE_KEY);
const wallet = process.env.POLYMARKET_WALLET_ADDRESS;
const publicClient = createPublicClient({
chain: polygon,
transport: http(process.env.RPC_URL),
});
const relayerHeaders = {
RELAYER_API_KEY: process.env.RELAYER_API_KEY,
RELAYER_API_KEY_ADDRESS: account.address,
};
// Request a plan.
const planResponse = await fetch(
`${COLLATERAL_RETURN_API}/v1/collateral-return/plan`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet }),
},
);
if (!planResponse.ok) throw new Error(await planResponse.text());
const plan = await planResponse.json();
// Inspect the plan and apply any application-specific limits before signing.
console.log({
netPusdOut: plan.net_pusd_out,
positionSummary: plan.position_summary,
estimatedCost: plan.estimated_cost,
truncated: plan.truncated,
});
// Fetch a fresh Proxy Wallet nonce and relay address.
const executeParamsResponse = await fetch(
`${RELAYER_API}/v1/account/transactions/params?address=${account.address}&type=PROXY`,
{ headers: relayerHeaders },
);
if (!executeParamsResponse.ok) {
throw new Error(await executeParamsResponse.text());
}
const executeParams = await executeParamsResponse.json();
// Encode and sign the Proxy Wallet relay request.
const data = encodeFunctionData({
abi: [
{
type: "function",
name: "proxy",
inputs: [
{
type: "tuple[]",
components: [
{ name: "typeCode", type: "uint8" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
],
},
],
outputs: [{ type: "bytes[]" }],
},
],
functionName: "proxy",
args: [
[
{
typeCode: 1,
to: plan.router_call.to,
value: 0n,
data: plan.router_call.data,
},
],
],
});
const relayerFee = 0n;
const gasPrice = 0n;
const gasLimit = await publicClient.estimateGas({
account: account.address,
to: PROXY_WALLET_FACTORY,
data,
});
const digest = keccak256(
concat([
toHex("rlx:"),
account.address,
PROXY_WALLET_FACTORY,
data,
toHex(relayerFee, { size: 32 }),
toHex(gasPrice, { size: 32 }),
toHex(gasLimit, { size: 32 }),
toHex(BigInt(executeParams.nonce), { size: 32 }),
RELAY_HUB,
executeParams.address,
]),
);
const signature = await account.signMessage({ message: { raw: digest } });
const envelope = {
type: "PROXY",
from: account.address,
to: PROXY_WALLET_FACTORY,
proxyWallet: wallet,
value: "0",
data,
nonce: executeParams.nonce,
signature,
metadata: "Return Combo collateral",
signatureParams: {
gasPrice: gasPrice.toString(),
relayerFee: relayerFee.toString(),
gasLimit: gasLimit.toString(),
relayHub: RELAY_HUB,
relay: executeParams.address,
},
};
// Execute the plan.
const submitResponse = await fetch(
`${COLLATERAL_RETURN_API}/v1/collateral-return/submit`,
{
method: "POST",
headers: { "Content-Type": "application/json", ...relayerHeaders },
body: JSON.stringify({ plan_hash: plan.plan_hash, envelope }),
},
);
if (!submitResponse.ok) throw new Error(await submitResponse.text());
const submitted = await submitResponse.json();
// Confirm the transaction.
let outcome;
for (;;) {
const transactionResponse = await fetch(
`${RELAYER_API}/v1/account/transactions/${submitted.transactionID}`,
{ headers: relayerHeaders },
);
if (!transactionResponse.ok) {
throw new Error(await transactionResponse.text());
}
const transaction = await transactionResponse.json();
if (transaction.state === "STATE_CONFIRMED") {
outcome = transaction;
break;
}
if (
transaction.state === "STATE_FAILED" ||
transaction.state === "STATE_INVALID"
) {
throw new Error(transaction.error_msg ?? transaction.state);
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
console.log(outcome);
Handle a Truncated Plan
The service limits how much work one transaction can contain. When more work remains, each plan is one complete, executable chunk—not a partial transaction. Execute and confirm each chunk before requesting the next plan because every new plan depends on the resulting onchain state.- TypeScript
- Python
- API
Use
plan.truncated to determine whether more work remains after the current
plan.let plan: CollateralReturnPlanResponse;
do {
plan = await client.planCollateralReturn();
// Inspect the return and residual-position impact, and apply any
// application-specific limits before signing.
const handle = await client.executeCollateralReturnPlan({ plan });
await handle.wait();
} while (plan.truncated);
Use
plan.truncated to determine whether more work remains after the current
plan.while True:
plan = await client.plan_collateral_return()
# Inspect the return and residual-position impact before signing.
handle = await client.execute_collateral_return_plan(plan=plan)
await handle.wait()
if not plan.truncated:
break
After the submitted transaction confirms, inspect the
truncated value from
the plan that produced it. When it is true, request a fresh plan, inspect and
sign its wallet action, submit it, and wait for confirmation again. Continue
until the confirmed plan has truncated: false.