391 lines
12 KiB
Markdown
391 lines
12 KiB
Markdown
> ## Documentation Index
|
|
> Fetch the complete documentation index at: https://docs.polymarket.com/llms.txt
|
|
> Use this file to discover all available pages before exploring further.
|
|
|
|
# Inventory Management
|
|
|
|
> Managing outcome token inventory for market making
|
|
|
|
Market makers need outcome tokens on both sides to quote a market. The three core inventory operations are **splitting** pUSD into YES/NO token pairs, **merging** pairs back into pUSD, and **redeeming** winning tokens after resolution — all executed gaslessly through the Relayer Client.
|
|
|
|
<Info>
|
|
For a full breakdown of how the Conditional Token Framework works, see [CTF
|
|
Overview](/trading/ctf/overview). This page focuses on the MM workflow using
|
|
the Relayer Client.
|
|
</Info>
|
|
|
|
***
|
|
|
|
## Splitting pUSD into Tokens
|
|
|
|
Split converts pUSD into equal amounts of YES and NO tokens — creating the inventory you need to quote both sides of a market.
|
|
|
|
<CodeGroup>
|
|
```typescript TypeScript theme={null}
|
|
import { ethers } from "ethers";
|
|
import { Interface } from "ethers/lib/utils";
|
|
import { RelayClient, Transaction } from "@polymarket/builder-relayer-client";
|
|
|
|
const CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
|
|
const pUSD_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB";
|
|
|
|
const ctfInterface = new Interface([
|
|
"function splitPosition(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint[] partition, uint amount)",
|
|
]);
|
|
|
|
// Split $1000 pUSD into YES/NO tokens
|
|
const amount = ethers.utils.parseUnits("1000", 6); // pUSD has 6 decimals
|
|
|
|
const splitTx: Transaction = {
|
|
to: CTF_ADDRESS,
|
|
data: ctfInterface.encodeFunctionData("splitPosition", [
|
|
pUSD_ADDRESS, // collateralToken
|
|
ethers.constants.HashZero, // parentCollectionId (always zero for Polymarket)
|
|
conditionId, // conditionId from market
|
|
[1, 2], // partition: [YES, NO]
|
|
amount,
|
|
]),
|
|
value: "0",
|
|
};
|
|
|
|
const response = await client.execute([splitTx], "Split pUSD into tokens");
|
|
const result = await response.wait();
|
|
console.log("Split completed:", result?.transactionHash);
|
|
```
|
|
|
|
```python Python theme={null}
|
|
from web3 import Web3
|
|
|
|
CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
|
pUSD_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
|
|
|
|
ctf_abi = [{
|
|
"name": "splitPosition",
|
|
"type": "function",
|
|
"inputs": [
|
|
{"name": "collateralToken", "type": "address"},
|
|
{"name": "parentCollectionId", "type": "bytes32"},
|
|
{"name": "conditionId", "type": "bytes32"},
|
|
{"name": "partition", "type": "uint256[]"},
|
|
{"name": "amount", "type": "uint256"}
|
|
],
|
|
"outputs": []
|
|
}]
|
|
|
|
# Split $1000 pUSD into YES/NO tokens
|
|
amount = 1000 * 10**6 # pUSD has 6 decimals
|
|
|
|
split_tx = {
|
|
"to": CTF_ADDRESS,
|
|
"data": Web3().eth.contract(
|
|
address=CTF_ADDRESS, abi=ctf_abi
|
|
).encode_abi(
|
|
abi_element_identifier="splitPosition",
|
|
args=[
|
|
pUSD_ADDRESS,
|
|
bytes(32), # parentCollectionId (always zero)
|
|
condition_id, # conditionId from market
|
|
[1, 2], # partition: [YES, NO]
|
|
amount,
|
|
]
|
|
),
|
|
"value": "0"
|
|
}
|
|
|
|
response = client.execute([split_tx], "Split pUSD into tokens")
|
|
response.wait()
|
|
```
|
|
|
|
```rust Rust theme={null}
|
|
use polymarket_client_sdk_v2::ctf::Client as CtfClient;
|
|
use polymarket_client_sdk_v2::ctf::types::SplitPositionRequest;
|
|
use polymarket_client_sdk_v2::types::{U256, address};
|
|
|
|
let ctf_client = CtfClient::new(provider, 137)?;
|
|
|
|
// Split $1000 pUSD into YES/NO tokens
|
|
let request = SplitPositionRequest::builder()
|
|
.collateral_token(address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"))
|
|
.condition_id(condition_id)
|
|
.partition(vec![U256::from(1), U256::from(2)])
|
|
.amount(U256::from(1000_000_000u64)) // 1000 pUSD (6 decimals)
|
|
.build();
|
|
let result = ctf_client.split_position(&request).await?;
|
|
println!("Split tx: {:?}", result.transaction_hash);
|
|
```
|
|
</CodeGroup>
|
|
|
|
After splitting 1000 pUSD, you receive 1000 YES tokens and 1000 NO tokens. Your pUSD balance decreases by 1000.
|
|
|
|
***
|
|
|
|
## Merging Tokens to pUSD
|
|
|
|
Merge converts equal amounts of YES and NO tokens back into pUSD — useful for reducing exposure, exiting a market, or freeing up capital.
|
|
|
|
<CodeGroup>
|
|
```typescript TypeScript theme={null}
|
|
const ctfInterface = new Interface([
|
|
"function mergePositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint[] partition, uint amount)",
|
|
]);
|
|
|
|
// Merge 500 YES + 500 NO back to 500 pUSD
|
|
const amount = ethers.utils.parseUnits("500", 6);
|
|
|
|
const mergeTx: Transaction = {
|
|
to: CTF_ADDRESS,
|
|
data: ctfInterface.encodeFunctionData("mergePositions", [
|
|
pUSD_ADDRESS,
|
|
ethers.constants.HashZero,
|
|
conditionId,
|
|
[1, 2],
|
|
amount,
|
|
]),
|
|
value: "0",
|
|
};
|
|
|
|
const response = await client.execute([mergeTx], "Merge tokens to pUSD");
|
|
await response.wait();
|
|
```
|
|
|
|
```python Python theme={null}
|
|
merge_abi = [{
|
|
"name": "mergePositions",
|
|
"type": "function",
|
|
"inputs": [
|
|
{"name": "collateralToken", "type": "address"},
|
|
{"name": "parentCollectionId", "type": "bytes32"},
|
|
{"name": "conditionId", "type": "bytes32"},
|
|
{"name": "partition", "type": "uint256[]"},
|
|
{"name": "amount", "type": "uint256"}
|
|
],
|
|
"outputs": []
|
|
}]
|
|
|
|
# Merge 500 YES + 500 NO back to 500 pUSD
|
|
amount = 500 * 10**6
|
|
|
|
merge_tx = {
|
|
"to": CTF_ADDRESS,
|
|
"data": Web3().eth.contract(
|
|
address=CTF_ADDRESS, abi=merge_abi
|
|
).encode_abi(
|
|
abi_element_identifier="mergePositions",
|
|
args=[pUSD_ADDRESS, bytes(32), condition_id, [1, 2], amount]
|
|
),
|
|
"value": "0"
|
|
}
|
|
|
|
response = client.execute([merge_tx], "Merge tokens to pUSD")
|
|
response.wait()
|
|
```
|
|
|
|
```rust Rust theme={null}
|
|
use polymarket_client_sdk_v2::ctf::types::MergePositionsRequest;
|
|
|
|
// Merge 500 YES + 500 NO back to 500 pUSD
|
|
let request = MergePositionsRequest::builder()
|
|
.collateral_token(address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"))
|
|
.condition_id(condition_id)
|
|
.partition(vec![U256::from(1), U256::from(2)])
|
|
.amount(U256::from(500_000_000u64)) // 500 pUSD (6 decimals)
|
|
.build();
|
|
let result = ctf_client.merge_positions(&request).await?;
|
|
```
|
|
</CodeGroup>
|
|
|
|
After merging 500 of each, your YES and NO balances decrease by 500 and your pUSD balance increases by 500.
|
|
|
|
***
|
|
|
|
## Redeeming After Resolution
|
|
|
|
Once a market resolves, redeem winning tokens for pUSD. Each winning token is worth $1 — losing tokens redeem for $0.
|
|
|
|
### Check Resolution Status
|
|
|
|
<CodeGroup>
|
|
```typescript TypeScript theme={null}
|
|
const market = await clobClient.getMarket(conditionId);
|
|
if (market.closed) {
|
|
const winningToken = market.tokens.find((t) => t.winner);
|
|
console.log("Winning outcome:", winningToken?.outcome);
|
|
}
|
|
```
|
|
|
|
```python Python theme={null}
|
|
market = clob_client.get_market(condition_id)
|
|
if market.get("closed"):
|
|
winning = next(t for t in market["tokens"] if t.get("winner"))
|
|
print("Winning outcome:", winning["outcome"])
|
|
```
|
|
|
|
```rust Rust theme={null}
|
|
let market = clob_client.market(condition_id).await?;
|
|
if market.closed {
|
|
if let Some(winner) = market.tokens.iter().find(|t| t.winner) {
|
|
println!("Winning outcome: {}", winner.outcome);
|
|
}
|
|
}
|
|
```
|
|
</CodeGroup>
|
|
|
|
### Redeem Winning Tokens
|
|
|
|
<CodeGroup>
|
|
```typescript TypeScript theme={null}
|
|
const ctfInterface = new Interface([
|
|
"function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint[] indexSets)",
|
|
]);
|
|
|
|
const redeemTx: Transaction = {
|
|
to: CTF_ADDRESS,
|
|
data: ctfInterface.encodeFunctionData("redeemPositions", [
|
|
pUSD_ADDRESS,
|
|
ethers.constants.HashZero,
|
|
conditionId,
|
|
[1, 2], // Redeem both YES and NO (only winners pay out)
|
|
]),
|
|
value: "0",
|
|
};
|
|
|
|
const response = await client.execute([redeemTx], "Redeem winning tokens");
|
|
await response.wait();
|
|
```
|
|
|
|
```python Python theme={null}
|
|
redeem_abi = [{
|
|
"name": "redeemPositions",
|
|
"type": "function",
|
|
"inputs": [
|
|
{"name": "collateralToken", "type": "address"},
|
|
{"name": "parentCollectionId", "type": "bytes32"},
|
|
{"name": "conditionId", "type": "bytes32"},
|
|
{"name": "indexSets", "type": "uint256[]"}
|
|
],
|
|
"outputs": []
|
|
}]
|
|
|
|
redeem_tx = {
|
|
"to": CTF_ADDRESS,
|
|
"data": Web3().eth.contract(
|
|
address=CTF_ADDRESS, abi=redeem_abi
|
|
).encode_abi(
|
|
abi_element_identifier="redeemPositions",
|
|
args=[pUSD_ADDRESS, bytes(32), condition_id, [1, 2]]
|
|
),
|
|
"value": "0"
|
|
}
|
|
|
|
response = client.execute([redeem_tx], "Redeem winning tokens")
|
|
response.wait()
|
|
```
|
|
|
|
```rust Rust theme={null}
|
|
use polymarket_client_sdk_v2::ctf::types::RedeemPositionsRequest;
|
|
|
|
let request = RedeemPositionsRequest::builder()
|
|
.collateral_token(address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"))
|
|
.condition_id(condition_id)
|
|
.index_sets(vec![U256::from(1), U256::from(2)]) // Redeem both (only winners pay)
|
|
.build();
|
|
let result = ctf_client.redeem_positions(&request).await?;
|
|
```
|
|
</CodeGroup>
|
|
|
|
***
|
|
|
|
## Negative Risk Markets
|
|
|
|
Multi-outcome markets use the Neg Risk CTF Exchange and Neg Risk Adapter. Split and merge work the same way, but use different contract addresses:
|
|
|
|
```typescript theme={null}
|
|
const NEG_RISK_CTF_EXCHANGE = "0xe2222d279d744050d28e00520010520000310F59";
|
|
const NEG_RISK_ADAPTER = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296";
|
|
```
|
|
|
|
See [Negative Risk Markets](/advanced/neg-risk) for details on how multi-outcome token mechanics differ.
|
|
|
|
***
|
|
|
|
## Inventory Strategies
|
|
|
|
### Before Quoting
|
|
|
|
1. Check market metadata via the [Gamma API](/market-data/fetching-markets)
|
|
2. Split sufficient pUSD to cover your expected quoting size
|
|
3. Set token approvals if not already done (see [Getting Started](/market-makers/getting-started))
|
|
|
|
### During Trading
|
|
|
|
* **Skew quotes** when inventory becomes imbalanced on one side
|
|
* **Merge excess tokens** to free up capital for other markets
|
|
* **Split more** when inventory on either side runs low
|
|
|
|
### After Resolution
|
|
|
|
1. Cancel all open orders in the market
|
|
2. Wait for resolution to complete
|
|
3. Redeem winning tokens
|
|
4. Merge any remaining YES/NO pairs
|
|
|
|
***
|
|
|
|
## Batch Operations
|
|
|
|
Execute multiple inventory operations in a single relayer call for efficiency:
|
|
|
|
```typescript theme={null}
|
|
const transactions: Transaction[] = [
|
|
// Split on Market A
|
|
{
|
|
to: CTF_ADDRESS,
|
|
data: ctfInterface.encodeFunctionData("splitPosition", [
|
|
pUSD_ADDRESS,
|
|
ethers.constants.HashZero,
|
|
conditionIdA,
|
|
[1, 2],
|
|
ethers.utils.parseUnits("1000", 6),
|
|
]),
|
|
value: "0",
|
|
},
|
|
// Split on Market B
|
|
{
|
|
to: CTF_ADDRESS,
|
|
data: ctfInterface.encodeFunctionData("splitPosition", [
|
|
pUSD_ADDRESS,
|
|
ethers.constants.HashZero,
|
|
conditionIdB,
|
|
[1, 2],
|
|
ethers.utils.parseUnits("1000", 6),
|
|
]),
|
|
value: "0",
|
|
},
|
|
];
|
|
|
|
const response = await client.execute(transactions, "Batch inventory setup");
|
|
await response.wait();
|
|
```
|
|
|
|
***
|
|
|
|
## Next Steps
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="CTF Overview" icon="coins" href="/trading/ctf/overview">
|
|
How the Conditional Token Framework works under the hood
|
|
</Card>
|
|
|
|
<Card title="Split Tokens" icon="scissors" href="/trading/ctf/split">
|
|
Detailed split function parameters and prerequisites
|
|
</Card>
|
|
|
|
<Card title="Merge Tokens" icon="merge" href="/trading/ctf/merge">
|
|
Detailed merge function parameters
|
|
</Card>
|
|
|
|
<Card title="Gasless Transactions" icon="gas-pump" href="/trading/gasless">
|
|
Relayer Client setup and configuration
|
|
</Card>
|
|
</CardGroup>
|