Skip to main content

AA Account Binding

Bind the caller's wallet account to an Account Abstraction (AA) smart account and session key. This is required for programmatic market-making clients that submit their own on-chain close-audit transactions. If the platform runs a backend close-settlement relayer, this binding is not required to close — see Who submits the on-chain close transaction.

The binding is stored per chain_id and user wallet address. After binding, GET /api/v1/unified/account returns the current aa_smart_account_address and aa_session_key_address.

Why this is required

Market makers usually trade through API keys and do not have a browser wallet available during normal order flow. The close-audit transaction still needs an AA smart account and session key, so the maker must register them once through the API.

The API key authenticates the request, but the binding itself must be approved by the owner wallet through an EIP-712 BindAAAccount signature. This prevents an API key alone from replacing the wallet's AA account.

Who submits the on-chain close transaction

Closing a position settles its collateral and realized PnL on the Vault. The backend always signs the settlement off-chain; the on-chain transaction can be submitted in one of two ways:

  • Client-submitted (the flow on this page). The market maker binds an AA smart account + session key (below) and submits the close transaction itself using the session key. Choose this when you run your own submitter and want to control the on-chain step.
  • Backend relayer (recommended for unattended bots). When the platform enables its close-settlement relayer, the backend broadcasts the settlement transaction itself, using its own signer. In this mode you just close through the normal trading endpoints — a reduceOnly / closePosition order on /fapi/v1/order, or a TP/SL order that the keeper triggers — and the on-chain settlement is completed for you. No UserOp submission and no AA binding are needed for closing.

Whether the relayer is enabled is a platform-side setting that differs per environment; check with the operator. When it is enabled, market makers do not need the binding flow below in order to close positions — closes settle on-chain automatically after the matching engine fills them.

Flow

  1. Generate or derive the AA smart account and session key off-chain.
  2. Call GET /api/v1/account/aa-binding/nonce with HMAC authentication.
  3. Sign the returned typed_data with the owner wallet.
  4. Call POST /api/v1/account/aa-binding with the same addresses, nonce, deadline, and EIP-712 signature.
  5. Confirm the binding with GET /api/v1/account/aa-binding or GET /api/v1/unified/account.

Get AA Binding Challenge

HTTP Request

GET /api/v1/account/aa-binding/nonce (HMAC SHA256)

Request Parameters

NameTypeRequiredDescription
smart_account_addressSTRINGYESAA smart account address to bind
session_key_addressSTRINGYESSession key address used by the AA close-audit flow
timestampLONGYESUnix timestamp in milliseconds
signatureSTRINGYESHMAC SHA-256 signature over the query string without signature

Response Example

{
"chain_id": 43113,
"user_address": "0xf0cb036c96a7118d879cf3a9e39bdc4ee1d1b4d8",
"smart_account_address": "0x0d1ab011cb5c270155127bb2280cc1ad0a0eea1b",
"session_key_address": "0x1111111111111111111111111111111111111111",
"nonce": 7,
"deadline": 1779999999,
"typed_data": {
"primaryType": "BindAAAccount",
"domain": {
"name": "Primit Vault AVAX Fuji",
"version": "1",
"chainId": 43113,
"verifyingContract": "0x0EBfBEA06D8658B76245F3408A1ECfE3f7d3C8d5"
},
"message": {
"user": "0xf0cb036c96a7118d879cf3a9e39bdc4ee1d1b4d8",
"smartAccount": "0x0d1ab011cb5c270155127bb2280cc1ad0a0eea1b",
"sessionKey": "0x1111111111111111111111111111111111111111",
"nonce": "7",
"deadline": "1779999999"
}
}
}

Bind AA Account

HTTP Request

POST /api/v1/account/aa-binding (HMAC SHA256)

For POST requests, the HMAC payload is the query string without signature followed by the raw JSON request body.

Request Parameters

NameTypeRequiredDescription
timestampLONGYESUnix timestamp in milliseconds
signatureSTRINGYESHMAC SHA-256 signature

Request Body

NameTypeRequiredDescription
chain_idLONGYESMust match the API environment chain ID
smart_account_addressSTRINGYESAA smart account address
session_key_addressSTRINGYESSession key address
nonceLONGYESNonce returned by the challenge endpoint
deadlineLONGYESUnix seconds deadline returned by the challenge endpoint
signatureSTRINGYESEIP-712 BindAAAccount signature from the owner wallet

Response Example

{
"chain_id": 43113,
"user_address": "0xf0cb036c96a7118d879cf3a9e39bdc4ee1d1b4d8",
"aa_smart_account_address": "0x0d1ab011cb5c270155127bb2280cc1ad0a0eea1b",
"aa_session_key_address": "0x1111111111111111111111111111111111111111"
}

Get Current Binding

HTTP Request

GET /api/v1/account/aa-binding (HMAC SHA256)

Response Example

{
"chain_id": 43113,
"user_address": "0xf0cb036c96a7118d879cf3a9e39bdc4ee1d1b4d8",
"aa_smart_account_address": "0x0d1ab011cb5c270155127bb2280cc1ad0a0eea1b",
"aa_session_key_address": "0x1111111111111111111111111111111111111111"
}

If the user has not bound an AA account, both AA fields are null.

EIP-712 Type

BindAAAccount(
address user,
address smartAccount,
address sessionKey,
uint256 nonce,
uint256 deadline
)

The signer must be the owner wallet represented by the API key. The user field is the wallet address, not the AA smart account address.

Error Codes

CodeMeaning
CHAIN_ID_MISMATCHchain_id does not match the API environment
INVALID_SMART_ACCOUNT_ADDRESSsmart_account_address is not a valid EVM address
INVALID_SESSION_KEY_ADDRESSsession_key_address is not a valid EVM address
AA_BINDING_SIGNATURE_EXPIREDdeadline has passed
AA_BINDING_NONCE_INVALIDNonce was not issued, already used, or superseded by a newer challenge
AA_BINDING_SIGNATURE_INVALIDEIP-712 signature is invalid or not signed by the owner wallet

Code Example

import json
import time
import hmac
import hashlib
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data

BASE_URL = "https://api.primit.xyz"
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
OWNER_PRIVATE_KEY = "0x..."
SMART_ACCOUNT = "0x0d1Ab011CB5c270155127bb2280CC1AD0A0Eea1B"
SESSION_KEY = "0x1111111111111111111111111111111111111111"

def hmac_sign(payload: str) -> str:
return hmac.new(API_SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()

def signed_get(path: str, params: dict):
params["timestamp"] = int(time.time() * 1000)
query = "&".join(f"{k}={v}" for k, v in params.items())
return requests.get(
f"{BASE_URL}{path}?{query}&signature={hmac_sign(query)}",
headers={"X-MBX-APIKEY": API_KEY},
)

challenge = signed_get(
"/api/v1/account/aa-binding/nonce",
{
"smart_account_address": SMART_ACCOUNT,
"session_key_address": SESSION_KEY,
},
).json()

typed_data = challenge["typed_data"]
owner_sig = Account.sign_message(
encode_typed_data(full_message=typed_data),
OWNER_PRIVATE_KEY,
).signature.hex()

body = {
"chain_id": challenge["chain_id"],
"smart_account_address": SMART_ACCOUNT,
"session_key_address": SESSION_KEY,
"nonce": challenge["nonce"],
"deadline": challenge["deadline"],
"signature": owner_sig,
}
body_raw = json.dumps(body, separators=(",", ":"))
query = f"timestamp={int(time.time() * 1000)}"
resp = requests.post(
f"{BASE_URL}/api/v1/account/aa-binding?{query}&signature={hmac_sign(query + body_raw)}",
headers={"X-MBX-APIKEY": API_KEY, "Content-Type": "application/json"},
data=body_raw,
)
print(resp.json())