curl --request POST \
--url https://builder.prod.bedrock.ostium.io/v1/orders \
--header 'Content-Type: application/json' \
--data '
{
"type": "open",
"chainId": 421614,
"intent": {
"collateral": "1000000000",
"openPrice": "2500000000000000000000",
"tp": "0",
"sl": "0",
"trader": "0x2222222222222222222222222222222222222222",
"leverage": 10000,
"pairIndex": 1,
"buy": true,
"isDayTrade": false,
"builder": "0x0000000000000000000000000000000000000000",
"builderFee": 0,
"slippageP": "50",
"nonce": "42",
"deadline": "1800000030"
},
"signature": "0x3045022100abcdef"
}
'import requests
url = "https://builder.prod.bedrock.ostium.io/v1/orders"
payload = {
"type": "open",
"chainId": 421614,
"intent": {
"collateral": "1000000000",
"openPrice": "2500000000000000000000",
"tp": "0",
"sl": "0",
"trader": "0x2222222222222222222222222222222222222222",
"leverage": 10000,
"pairIndex": 1,
"buy": True,
"isDayTrade": False,
"builder": "0x0000000000000000000000000000000000000000",
"builderFee": 0,
"slippageP": "50",
"nonce": "42",
"deadline": "1800000030"
},
"signature": "0x3045022100abcdef"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'open',
chainId: 421614,
intent: {
collateral: '1000000000',
openPrice: '2500000000000000000000',
tp: '0',
sl: '0',
trader: '0x2222222222222222222222222222222222222222',
leverage: 10000,
pairIndex: 1,
buy: true,
isDayTrade: false,
builder: '0x0000000000000000000000000000000000000000',
builderFee: 0,
slippageP: '50',
nonce: '42',
deadline: '1800000030'
},
signature: '0x3045022100abcdef'
})
};
fetch('https://builder.prod.bedrock.ostium.io/v1/orders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://builder.prod.bedrock.ostium.io/v1/orders",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'type' => 'open',
'chainId' => 421614,
'intent' => [
'collateral' => '1000000000',
'openPrice' => '2500000000000000000000',
'tp' => '0',
'sl' => '0',
'trader' => '0x2222222222222222222222222222222222222222',
'leverage' => 10000,
'pairIndex' => 1,
'buy' => true,
'isDayTrade' => false,
'builder' => '0x0000000000000000000000000000000000000000',
'builderFee' => 0,
'slippageP' => '50',
'nonce' => '42',
'deadline' => '1800000030'
],
'signature' => '0x3045022100abcdef'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://builder.prod.bedrock.ostium.io/v1/orders"
payload := strings.NewReader("{\n \"type\": \"open\",\n \"chainId\": 421614,\n \"intent\": {\n \"collateral\": \"1000000000\",\n \"openPrice\": \"2500000000000000000000\",\n \"tp\": \"0\",\n \"sl\": \"0\",\n \"trader\": \"0x2222222222222222222222222222222222222222\",\n \"leverage\": 10000,\n \"pairIndex\": 1,\n \"buy\": true,\n \"isDayTrade\": false,\n \"builder\": \"0x0000000000000000000000000000000000000000\",\n \"builderFee\": 0,\n \"slippageP\": \"50\",\n \"nonce\": \"42\",\n \"deadline\": \"1800000030\"\n },\n \"signature\": \"0x3045022100abcdef\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://builder.prod.bedrock.ostium.io/v1/orders")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"open\",\n \"chainId\": 421614,\n \"intent\": {\n \"collateral\": \"1000000000\",\n \"openPrice\": \"2500000000000000000000\",\n \"tp\": \"0\",\n \"sl\": \"0\",\n \"trader\": \"0x2222222222222222222222222222222222222222\",\n \"leverage\": 10000,\n \"pairIndex\": 1,\n \"buy\": true,\n \"isDayTrade\": false,\n \"builder\": \"0x0000000000000000000000000000000000000000\",\n \"builderFee\": 0,\n \"slippageP\": \"50\",\n \"nonce\": \"42\",\n \"deadline\": \"1800000030\"\n },\n \"signature\": \"0x3045022100abcdef\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://builder.prod.bedrock.ostium.io/v1/orders")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"open\",\n \"chainId\": 421614,\n \"intent\": {\n \"collateral\": \"1000000000\",\n \"openPrice\": \"2500000000000000000000\",\n \"tp\": \"0\",\n \"sl\": \"0\",\n \"trader\": \"0x2222222222222222222222222222222222222222\",\n \"leverage\": 10000,\n \"pairIndex\": 1,\n \"buy\": true,\n \"isDayTrade\": false,\n \"builder\": \"0x0000000000000000000000000000000000000000\",\n \"builderFee\": 0,\n \"slippageP\": \"50\",\n \"nonce\": \"42\",\n \"deadline\": \"1800000030\"\n },\n \"signature\": \"0x3045022100abcdef\"\n}"
response = http.request(request)
puts response.read_body{
"status": "filled",
"userOpHash": "0xaaaa",
"transactionHash": "0xbbbb",
"execution": {
"tradeId": "150885",
"tradeIndex": 0,
"fillPrice": "79680.281023459830864072",
"collateral": "98.9",
"tradeNotional": "0.01241210481811446"
}
}{
"error": "Bad Request",
"message": "Validation failed",
"issues": []
}{
"error": "Unauthorized",
"message": "signature does not match the intent"
}{
"error": "Forbidden",
"message": "trader is not a permitted partner"
}{
"error": "Payload Too Large",
"message": "request entity too large"
}{
"error": "Too Many Requests",
"message": "Rate limit exceeded. Try again in 7s."
}{
"error": "Internal Server Error",
"message": "Internal Server Error"
}Submit an order
One endpoint, three actions. type picks between opening a position, closing one (fully or partially), and withdrawing collateral from one. Pick the variant in the request body below to see the fields each needs.
Reading the response
status is the answer, not the HTTP code. A 200 means the order was submitted and settled; status says how it settled:
status | What happened |
|---|---|
filled | It executed. execution carries the tradeId and the resulting amounts. |
reverted | It reached the chain and changed nothing. reason says why. |
Signing
Every intent is an EIP-712 signature under this domain:
{
"name": "OstiumAtomicTrading",
"version": "1",
"chainId": 42161,
"verifyingContract": "0x5eB3960C3fd3274cD81fE5972e0de01084bDa325"
}
On Arbitrum Sepolia (421614) it is a different contract, so the domain differs too:
{
"name": "OstiumAtomicTrading",
"version": "1",
"chainId": 421614,
"verifyingContract": "0x32C06a3eC2A40DABf6A8f645f29321cB7236DAA3"
}
The struct you sign is the intent object itself, and its primaryType is named after the type you send:
type | primaryType |
|---|---|
open | OpenIntent |
close | CloseIntent |
removeCollateral | RemoveCollateralIntent |
Declare the fields in the order the request body lists them, and with exactly these solidity widths. EIP-712 hashes the field names AND types into a single typehash, so a different order — or uint256 where the contract says uint192 — is a different type: the digest changes and the contract cannot match your signature. The JSON schema below shows string and integer, which cannot express the widths, so take them from here:
OpenIntent(uint256 collateral,uint192 openPrice,uint192 tp,uint192 sl,address trader,
uint32 leverage,uint16 pairIndex,bool buy,bool isDayTrade,address builder,
uint32 builderFee,uint256 slippageP,uint256 nonce,uint256 deadline)
CloseIntent(address trader,uint16 pairIndex,uint8 index,uint256 tradeId,
uint16 closePercentage,uint192 marketPrice,uint32 slippageP,uint256 nonce,
uint256 deadline)
RemoveCollateralIntent(address trader,uint16 pairIndex,uint8 index,uint256 tradeId,
uint256 removeAmount,uint256 nonce,uint256 deadline)
(Line breaks above are for reading only — the canonical type string has none.)
As a viem signTypedData call:
await account.signTypedData({
domain, // the OstiumAtomicTrading domain above
primaryType: 'OpenIntent',
types: {
OpenIntent: [
{ name: 'collateral', type: 'uint256' },
{ name: 'openPrice', type: 'uint192' },
{ name: 'tp', type: 'uint192' },
{ name: 'sl', type: 'uint192' },
{ name: 'trader', type: 'address' },
{ name: 'leverage', type: 'uint32' },
{ name: 'pairIndex', type: 'uint16' },
{ name: 'buy', type: 'bool' },
{ name: 'isDayTrade', type: 'bool' },
{ name: 'builder', type: 'address' },
{ name: 'builderFee', type: 'uint32' },
{ name: 'slippageP', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
message: intent,
});
To check your work without spending anything, the wrapper exposes hashOpenIntent, hashCloseIntent and hashRemoveCollateralIntent as view functions — a local digest that matches those is a signature the contract will accept.
Sign as trader: an EOA, or a contract wallet that answers EIP-1271.
One trap the schema cannot warn you about. slippageP is a uint256 on OpenIntent but a uint32 on CloseIntent. The contract types them differently, and signing the wrong width silently changes the digest.
Limits
Checked before anything is submitted, and tighter than the contract itself — an intent the contract would accept can still be refused here:
| Field | Allowed | Meaning |
|---|---|---|
slippageP | 0 < slippageP ≤ 100 | scaled by 10000, so 100 is 1% — the most you can tolerate |
closePercentage | 0 < closePercentage ≤ 10000 | scaled by 10000, so 10000 is 100% and 5000 is half |
deadline | now < deadline ≤ now + 60s | unix seconds |
Both percentages use the same base of 10000. Neither may be 0: a zero slippageP cannot fill, and the protocol reads a zero closePercentage as a full close.
Before your first order
The trader must be an allowlisted partner, and onboarded once per chain through POST /v1/onboard. Without the delegation an order reaches the chain and reverts as NotDelegate.
Field notes
- Send amounts as strings of plain digits —
"50000000", not5e7or50000000. They are too large for a JSON number, and rounding one changes the digest. - Addresses can be any casing. Checksummed, lowercase or uppercase all work.
- Every field is signed,
builderandbuilderFeeincluded. Nothing can be added or altered between you and the contract.
Rate limit: 30 requests per 10 seconds per IP. Read x-ratelimit-* for the live budget rather than assuming this figure.
curl --request POST \
--url https://builder.prod.bedrock.ostium.io/v1/orders \
--header 'Content-Type: application/json' \
--data '
{
"type": "open",
"chainId": 421614,
"intent": {
"collateral": "1000000000",
"openPrice": "2500000000000000000000",
"tp": "0",
"sl": "0",
"trader": "0x2222222222222222222222222222222222222222",
"leverage": 10000,
"pairIndex": 1,
"buy": true,
"isDayTrade": false,
"builder": "0x0000000000000000000000000000000000000000",
"builderFee": 0,
"slippageP": "50",
"nonce": "42",
"deadline": "1800000030"
},
"signature": "0x3045022100abcdef"
}
'import requests
url = "https://builder.prod.bedrock.ostium.io/v1/orders"
payload = {
"type": "open",
"chainId": 421614,
"intent": {
"collateral": "1000000000",
"openPrice": "2500000000000000000000",
"tp": "0",
"sl": "0",
"trader": "0x2222222222222222222222222222222222222222",
"leverage": 10000,
"pairIndex": 1,
"buy": True,
"isDayTrade": False,
"builder": "0x0000000000000000000000000000000000000000",
"builderFee": 0,
"slippageP": "50",
"nonce": "42",
"deadline": "1800000030"
},
"signature": "0x3045022100abcdef"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'open',
chainId: 421614,
intent: {
collateral: '1000000000',
openPrice: '2500000000000000000000',
tp: '0',
sl: '0',
trader: '0x2222222222222222222222222222222222222222',
leverage: 10000,
pairIndex: 1,
buy: true,
isDayTrade: false,
builder: '0x0000000000000000000000000000000000000000',
builderFee: 0,
slippageP: '50',
nonce: '42',
deadline: '1800000030'
},
signature: '0x3045022100abcdef'
})
};
fetch('https://builder.prod.bedrock.ostium.io/v1/orders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://builder.prod.bedrock.ostium.io/v1/orders",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'type' => 'open',
'chainId' => 421614,
'intent' => [
'collateral' => '1000000000',
'openPrice' => '2500000000000000000000',
'tp' => '0',
'sl' => '0',
'trader' => '0x2222222222222222222222222222222222222222',
'leverage' => 10000,
'pairIndex' => 1,
'buy' => true,
'isDayTrade' => false,
'builder' => '0x0000000000000000000000000000000000000000',
'builderFee' => 0,
'slippageP' => '50',
'nonce' => '42',
'deadline' => '1800000030'
],
'signature' => '0x3045022100abcdef'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://builder.prod.bedrock.ostium.io/v1/orders"
payload := strings.NewReader("{\n \"type\": \"open\",\n \"chainId\": 421614,\n \"intent\": {\n \"collateral\": \"1000000000\",\n \"openPrice\": \"2500000000000000000000\",\n \"tp\": \"0\",\n \"sl\": \"0\",\n \"trader\": \"0x2222222222222222222222222222222222222222\",\n \"leverage\": 10000,\n \"pairIndex\": 1,\n \"buy\": true,\n \"isDayTrade\": false,\n \"builder\": \"0x0000000000000000000000000000000000000000\",\n \"builderFee\": 0,\n \"slippageP\": \"50\",\n \"nonce\": \"42\",\n \"deadline\": \"1800000030\"\n },\n \"signature\": \"0x3045022100abcdef\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://builder.prod.bedrock.ostium.io/v1/orders")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"open\",\n \"chainId\": 421614,\n \"intent\": {\n \"collateral\": \"1000000000\",\n \"openPrice\": \"2500000000000000000000\",\n \"tp\": \"0\",\n \"sl\": \"0\",\n \"trader\": \"0x2222222222222222222222222222222222222222\",\n \"leverage\": 10000,\n \"pairIndex\": 1,\n \"buy\": true,\n \"isDayTrade\": false,\n \"builder\": \"0x0000000000000000000000000000000000000000\",\n \"builderFee\": 0,\n \"slippageP\": \"50\",\n \"nonce\": \"42\",\n \"deadline\": \"1800000030\"\n },\n \"signature\": \"0x3045022100abcdef\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://builder.prod.bedrock.ostium.io/v1/orders")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"open\",\n \"chainId\": 421614,\n \"intent\": {\n \"collateral\": \"1000000000\",\n \"openPrice\": \"2500000000000000000000\",\n \"tp\": \"0\",\n \"sl\": \"0\",\n \"trader\": \"0x2222222222222222222222222222222222222222\",\n \"leverage\": 10000,\n \"pairIndex\": 1,\n \"buy\": true,\n \"isDayTrade\": false,\n \"builder\": \"0x0000000000000000000000000000000000000000\",\n \"builderFee\": 0,\n \"slippageP\": \"50\",\n \"nonce\": \"42\",\n \"deadline\": \"1800000030\"\n },\n \"signature\": \"0x3045022100abcdef\"\n}"
response = http.request(request)
puts response.read_body{
"status": "filled",
"userOpHash": "0xaaaa",
"transactionHash": "0xbbbb",
"execution": {
"tradeId": "150885",
"tradeIndex": 0,
"fillPrice": "79680.281023459830864072",
"collateral": "98.9",
"tradeNotional": "0.01241210481811446"
}
}{
"error": "Bad Request",
"message": "Validation failed",
"issues": []
}{
"error": "Unauthorized",
"message": "signature does not match the intent"
}{
"error": "Forbidden",
"message": "trader is not a permitted partner"
}{
"error": "Payload Too Large",
"message": "request entity too large"
}{
"error": "Too Many Requests",
"message": "Rate limit exceeded. Try again in 7s."
}{
"error": "Internal Server Error",
"message": "Internal Server Error"
}Body
- Open
- Close
- Remove collateral
open Which chain to trade on. Arbitrum One is 42161, Arbitrum Sepolia 421614.
42161 421614
Show child attributes
Show child attributes
EIP-712 signature: 65-byte ECDSA, or opaque bytes for an ERC-1271 contract wallet
^0x([a-fA-F0-9]{2})+$Response
Terminal outcome of the submission
filled means the order executed. reverted means it reached the chain and changed nothing.
filled, reverted Identifies the ERC-4337 user operation we submitted for you. Quote it if you need to ask us about a specific order.
"0x8c8b184c3208dd11c0275a0cd5a62d0ef0fb4578d1f9b36569b2d346048af4f7"
The transaction it landed in — look it up on Arbiscan.
"0x9cae473bf1c8d54da1d32138e7bbe1d2ca05213c98fe70945b402634aac13279"
Why it reverted, decoded where we recognise the error. Present only when status is reverted.
"NotDelegate(0x98279066…, 0x32C06a3e…) — the trader has not delegated"
What the order executed as. Present only when status is filled, and the fields depend on the type. Amounts are decimal strings, already scaled.
Show child attributes
Show child attributes