curl --request POST \
--url https://api.numofx.com/v1/deposits \
--header 'Content-Type: application/json' \
--data '
{
"action": {
"subaccount_id": "<string>",
"nonce": "<string>",
"module": "<string>",
"data": "<string>",
"expiry": "<string>",
"owner": "<string>",
"signer": "<string>"
},
"signature": "<string>"
}
'import requests
url = "https://api.numofx.com/v1/deposits"
payload = {
"action": {
"subaccount_id": "<string>",
"nonce": "<string>",
"module": "<string>",
"data": "<string>",
"expiry": "<string>",
"owner": "<string>",
"signer": "<string>"
},
"signature": "<string>"
}
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({
action: {
subaccount_id: '<string>',
nonce: '<string>',
module: '<string>',
data: '<string>',
expiry: '<string>',
owner: '<string>',
signer: '<string>'
},
signature: '<string>'
})
};
fetch('https://api.numofx.com/v1/deposits', 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://api.numofx.com/v1/deposits",
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([
'action' => [
'subaccount_id' => '<string>',
'nonce' => '<string>',
'module' => '<string>',
'data' => '<string>',
'expiry' => '<string>',
'owner' => '<string>',
'signer' => '<string>'
],
'signature' => '<string>'
]),
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://api.numofx.com/v1/deposits"
payload := strings.NewReader("{\n \"action\": {\n \"subaccount_id\": \"<string>\",\n \"nonce\": \"<string>\",\n \"module\": \"<string>\",\n \"data\": \"<string>\",\n \"expiry\": \"<string>\",\n \"owner\": \"<string>\",\n \"signer\": \"<string>\"\n },\n \"signature\": \"<string>\"\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://api.numofx.com/v1/deposits")
.header("Content-Type", "application/json")
.body("{\n \"action\": {\n \"subaccount_id\": \"<string>\",\n \"nonce\": \"<string>\",\n \"module\": \"<string>\",\n \"data\": \"<string>\",\n \"expiry\": \"<string>\",\n \"owner\": \"<string>\",\n \"signer\": \"<string>\"\n },\n \"signature\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.numofx.com/v1/deposits")
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 \"action\": {\n \"subaccount_id\": \"<string>\",\n \"nonce\": \"<string>\",\n \"module\": \"<string>\",\n \"data\": \"<string>\",\n \"expiry\": \"<string>\",\n \"owner\": \"<string>\",\n \"signer\": \"<string>\"\n },\n \"signature\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"accepted": true,
"tx_hash": "<string>",
"amount_usdc": "<string>",
"amount_units": "<string>",
"credited_cash_e18": "<string>",
"action_hash": "<string>",
"status": "pending",
"receipt_status": "success",
"block_number": "<string>",
"subaccount_id": "<string>",
"permit_tx_hash": "<string>"
}{
"action_hash": "<string>",
"status": "pending",
"owner": "<string>",
"amount_usdc": "<string>",
"amount_units": "<string>",
"credited_cash_e18": "<string>",
"nonce": "<string>",
"subaccount_id_requested": "<string>",
"subaccount_id": "<string>",
"tx_hash": "<string>",
"permit_tx_hash": "<string>",
"block_number": "<string>",
"error": "<string>",
"revert": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
}{
"error": "<string>",
"revert": "<string>"
}{
"error": "<string>"
}{
"error": "<string>",
"revert": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Create a deposit
Opens a perp margin account (subaccount_id 0) and funds it, or tops one up, with one signed DepositModule action. The venue verifies the action, reads the nonce, ownership and the owner’s USDC balance and allowance from the chain, simulates, and submits it through Matching.verifyAndMatch, paying the gas.
curl --request POST \
--url https://api.numofx.com/v1/deposits \
--header 'Content-Type: application/json' \
--data '
{
"action": {
"subaccount_id": "<string>",
"nonce": "<string>",
"module": "<string>",
"data": "<string>",
"expiry": "<string>",
"owner": "<string>",
"signer": "<string>"
},
"signature": "<string>"
}
'import requests
url = "https://api.numofx.com/v1/deposits"
payload = {
"action": {
"subaccount_id": "<string>",
"nonce": "<string>",
"module": "<string>",
"data": "<string>",
"expiry": "<string>",
"owner": "<string>",
"signer": "<string>"
},
"signature": "<string>"
}
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({
action: {
subaccount_id: '<string>',
nonce: '<string>',
module: '<string>',
data: '<string>',
expiry: '<string>',
owner: '<string>',
signer: '<string>'
},
signature: '<string>'
})
};
fetch('https://api.numofx.com/v1/deposits', 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://api.numofx.com/v1/deposits",
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([
'action' => [
'subaccount_id' => '<string>',
'nonce' => '<string>',
'module' => '<string>',
'data' => '<string>',
'expiry' => '<string>',
'owner' => '<string>',
'signer' => '<string>'
],
'signature' => '<string>'
]),
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://api.numofx.com/v1/deposits"
payload := strings.NewReader("{\n \"action\": {\n \"subaccount_id\": \"<string>\",\n \"nonce\": \"<string>\",\n \"module\": \"<string>\",\n \"data\": \"<string>\",\n \"expiry\": \"<string>\",\n \"owner\": \"<string>\",\n \"signer\": \"<string>\"\n },\n \"signature\": \"<string>\"\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://api.numofx.com/v1/deposits")
.header("Content-Type", "application/json")
.body("{\n \"action\": {\n \"subaccount_id\": \"<string>\",\n \"nonce\": \"<string>\",\n \"module\": \"<string>\",\n \"data\": \"<string>\",\n \"expiry\": \"<string>\",\n \"owner\": \"<string>\",\n \"signer\": \"<string>\"\n },\n \"signature\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.numofx.com/v1/deposits")
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 \"action\": {\n \"subaccount_id\": \"<string>\",\n \"nonce\": \"<string>\",\n \"module\": \"<string>\",\n \"data\": \"<string>\",\n \"expiry\": \"<string>\",\n \"owner\": \"<string>\",\n \"signer\": \"<string>\"\n },\n \"signature\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"accepted": true,
"tx_hash": "<string>",
"amount_usdc": "<string>",
"amount_units": "<string>",
"credited_cash_e18": "<string>",
"action_hash": "<string>",
"status": "pending",
"receipt_status": "success",
"block_number": "<string>",
"subaccount_id": "<string>",
"permit_tx_hash": "<string>"
}{
"action_hash": "<string>",
"status": "pending",
"owner": "<string>",
"amount_usdc": "<string>",
"amount_units": "<string>",
"credited_cash_e18": "<string>",
"nonce": "<string>",
"subaccount_id_requested": "<string>",
"subaccount_id": "<string>",
"tx_hash": "<string>",
"permit_tx_hash": "<string>",
"block_number": "<string>",
"error": "<string>",
"revert": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
}{
"error": "<string>",
"revert": "<string>"
}{
"error": "<string>"
}{
"error": "<string>",
"revert": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Action for the
DepositModule. The venue verifies it and submits it through Matching.verifyAndMatch, paying the gas; the module
pulls your USDC, deposits it into the perp’s cash, and Matching records you as the account’s owner.
DepositModule once, on chain, or sign a permit with the
deposit and never send a transaction yourself. Either way every deposit is this endpoint, and the venue pays its gas.SignatureChecker, which treats any address that has code — including an EOA with
a 7702 delegation — as a contract wallet and asks it via ERC-1271 instead of recovering the signature. The deposit
is refused with OV_InvalidSignature unless the delegate contract implements isValidSignature. Use an
undelegated EOA, or a wallet that supports ERC-1271. The same applies to orders and withdrawals.Before your first deposit
Approve theDepositModule to move at least the amount you will deposit, from the wallet that will own the account:
USDC.approve(0x6540f8d9Eb599b045C05E45cb6a5B1730a806658, amount)
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.
Or sign a permit instead
USDC on Base supports EIP-2612 permits, so you can skip the on-chain approve entirely: sign aPermit with USDC’s
domain (name USD Coin, version 2, chain 8453) for spender = the DepositModule, a value of at least the
deposit amount, and your current USDC nonce. Send it alongside the action:
"permit": { "value": "1000000000", "deadline": "1789400600", "signature": "0x…" }
Signing the action
Sign anAction against the same Matching domain as an order, with:
| Field | Value |
|---|---|
subaccountId | 0 to open a new perp account; your perp account’s id to top it up |
nonce | any unused uint256; nonces are per owner and per module |
module | the DepositModule: 0x6540f8d9Eb599b045C05E45cb6a5B1730a806658 on Base |
data | abi.encode(uint256 amount, address asset, address managerForNewAccount) |
expiry | unix seconds, in the future and at most one hour ahead |
owner | your address; the USDC is pulled from here, and you own the account |
signer | the same as owner (session keys are not supported for deposits) |
data:
amountis USDC in 6-decimal base units:1000000000is 1,000 USDC. It must be explicit — the “whole balance” sentineltype(uint256).maxis refused — and at least the venue’s minimum (10 USDC).assetis the perp’s cash asset,0xA74E49b4Ed7cb176bc02ef4D8a1A3240C9aD4272— thequote_asset_addressthatGET /v1/marketsreports forcNGN-PERP. No other asset is accepted.managerForNewAccountis the perp risk manager,0xDE0423D0a1E15536265C9513d2e0c10DAb5835D4. For a top-up it is ignored; send the same address or zero.
1000000000000000000000. The
response states the amount in all three forms.
Send the action’s seven fields as strings (numbers in base 10, data as hex) with the signature:
{
"action": {
"subaccount_id": "0",
"nonce": "7",
"module": "0x6540f8d9Eb599b045C05E45cb6a5B1730a806658",
"data": "0x000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a74e49b4ed7cb176bc02ef4d8a1a3240c9ad4272000000000000000000000000de0423d0a1e15536265c9513d2e0c10dab5835d4",
"expiry": "1789400600",
"owner": "0xYourAddress",
"signer": "0xYourAddress"
},
"signature": "0x…"
}
What is checked
In order, and every check fails closed:- The request: module, signer equals owner, expiry,
dataexactly three words, the perp cash asset, an explicit amount at or above the minimum, and the perp risk manager for a new account. Refused with 400. - At most one deposit in progress per owner, three requests a minute, and six submitted deposits an hour. Refused
with 429; the hourly limit sends
Retry-After. A request refused for any other reason does not count toward the hour. - The signature authorizes the action. Refused with 401.
- Read from the chain before anything is submitted, each refused with 400 and what to do: the nonce is unused;
for a top-up, you own the account and it is a perp account; your wallet holds the USDC and has approved the
DepositModulefor it. - The executor’s simulation. The reverts above, if they still happen (a race), come back as 400 with
revertnaming them; any other revert is 422.
error says which; retry later.
The outcome
A 200 carries the transaction and what it credited:{
"action_hash": "0x1a818d05…",
"status": "confirmed",
"accepted": true,
"tx_hash": "0x…",
"receipt_status": "success",
"block_number": "52386004",
"subaccount_id": "27",
"amount_usdc": "1000.000000",
"amount_units": "1000000000",
"credited_cash_e18": "1000000000000000000000"
}
permit_tx_hash.
subaccount_id is your new account when you sent 0. Use it as subaccount_id on your orders.
action_hash identifies the deposit: it is the EIP-712 hash of your signed action (Matching.getActionHash).
status is one of:
status | Meaning |
|---|---|
pending | Being checked and submitted. Answered with 202. |
submitted | Broadcast, but the receipt was not known yet (receipt_status: "timeout"). |
confirmed | Mined successfully. |
reverted | Mined and reverted. |
rejected | Refused before anything was sent. You may resubmit the same request. |
unknown | The venue could not confirm whether it was submitted. |
Checking a deposit later
GET /v1/deposits/{action_hash}
submitted deposit is re-read from the chain first, so a receipt that timed out
resolves to confirmed (with subaccount_id) or reverted on the next read.
Sending the same signed request again is also safe at any point: the venue answers it from its record and
never submits it twice — after a timeout, a dropped connection or a restart. A 502 means the venue could not
confirm whether the deposit was submitted: look it up by action_hash, or resend the same request, before signing a
new one. A second signed deposit is a second deposit.Body
A Matching Action for the DepositModule, signed with EIP-712 against the Matching domain.
Show child attributes
Show child attributes
EIP-712 signature by signer: 65 bytes, or longer for an ERC-1271 wallet.
Optional EIP-2612 permit on Base USDC from owner to the DepositModule, instead of a prior approve. The venue submits it with the DepositModule as spender, and skips it when the allowance already covers the deposit.
Show child attributes
Show child attributes
Response
Submitted. receipt_status timeout means broadcast but not yet confirmed; resubmit the same request for the outcome.
True once mined successfully.
The deposit in USDC, with 6 decimal places.
The same amount in 6-decimal USDC base units, as signed.
What the account's cash rises by, at the ledger's 18 decimals.
EIP-712 hash of the signed action (Matching.getActionHash); the key for GET /v1/deposits/{action_hash}.
pending, submitted, confirmed, reverted, rejected, unknown timeout: broadcast, not yet confirmed.
success, reverted, timeout The account credited: your new account when subaccount_id was 0. Absent until the receipt is known.
The permit's transaction, when the deposit carried a permit and it was needed.
Was this page helpful?

