Swagger

Private tokens

3 endpoints.

3 endpoints

Private tokens

GET/private/tokensList private tokens

The active private tokens for a network, for example esUSDT. Read the symbol from here rather than hard-coding it: it is what every other private endpoint takes, and the set is per network.

  • Each entry carries the encrypted-token and registrar contract addresses, the 1:1 public underlying with its decimals, the private token's own internal precision, and display metadata.
  • One encrypted-token contract serves every token on a network — the underlying binds on first deposit — so eercAddress repeats across entries.
  • eercDecimals is that contract's single converter-wide precision, NOT the underlying's. A deposit converts only whole multiples of it and returns the remainder as dust: a visible remainder on a high-priced underlying, a cent on a stablecoin.

Authentication: X-API-Key

Input — query and path parameters

networkId string required
Chain ID (e.g. 43114 for Avalanche)

Output — example response (200)

{
  "tokens": [
    {
      "symbol": "esUSDT",
      "displayName": "ES USDT",
      "networkId": "43114",
      "eercAddress": "0x10dFB26576d490FF985f61B4Ec3F8DA65aF7B656",
      "registrarAddress": "0x906FAF1D7AD681c92E37B4843A18c56a56f0c85d",
      "underlyingAddress": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
      "underlyingSymbol": "USDT",
      "underlyingDecimals": 6,
      "eercDecimals": 2,
      "iconUrl": "https://assets.extrasafe.chat/crypto/private-tokens/43114/esUSDT/icon.svg"
    }
  ],
  "count": 1
}

Output — every status this endpoint answers with

StatusWhen
200Private tokens
400Invalid request body or query parameters
401Missing or invalid credentials
429Rate limit exceeded — retry after the number of seconds in the `Retry-After` response header
500Internal server error
Open in Swagger
POST/private/quoteQuote a private operation

What the account must hold in the PUBLIC underlying for a private operation to pass validation, asked BEFORE the client builds its proof. This is the call to gate a Send button on.

  • It exists because BOTH fees — the product fee and the rail fee — are taken in the public underlying, so an account can hold plenty of the private token and still be refused for want of the public one.
  • /private/prepare cannot answer this: it needs the proof, which is the expensive part of the flow.
  • requiredRaw is a CEILING. It reserves against the worst-case network fee, because the signed terms are capped on that same maximum — quoting the expected fee here would enable a send the backend then refuses.
  • sufficient compares requiredRaw against balanceRaw, so a client can gate on one boolean.
  • 404 when the symbol names no active private token for the network.

Authentication: X-API-Key

Input — request body

ownerAddress string required
The account the operation is for
networkId string required
Chain id
tokenSymbol string required
Private token symbol from GET /private/tokens
operationKind string required
One of private_deposit | private_transfer | private_withdraw | private_register
amountRaw string
Public underlying amount in base units. Required for deposit and withdraw; omit for a private transfer and for register.

Input — example request

{
  "ownerAddress": "0x098afb6194f54a1ea0097fca5c9593b5ab81348e",
  "networkId": "43114",
  "tokenSymbol": "esUSDT",
  "operationKind": "private_deposit",
  "amountRaw": "1000000"
}

Output — example response (200)

{
  "feeToken": {
    "address": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
    "symbol": "USDT",
    "decimals": 6
  },
  "productFeeRaw": "2500",
  "networkFeeRaw": "52000",
  "requiredRaw": "1054500",
  "balanceRaw": "5000000",
  "sufficient": true
}

Output — every status this endpoint answers with

StatusWhen
200The operation's cost in the public underlying
400Invalid body
401Missing or invalid API key
404Unknown private token for this network
Open in Swagger
POST/private/preparePrepare a private-token operation

Wraps client-built encrypted-token calldata — a deposit, a private transfer, a withdraw, or the one-time register activation — into a single gasless operation. THE RESPONSE SHAPE FOLLOWS THE RAIL, so branch on the presence of rail, then poll GET /transaction/:id like any other operation.

  • No rail field: the UserOperation rail. Sign userOperationHash and submit.
  • rail "forward_request": the executor rail. Sign the EIP-712 signingPayload with eth_signTypedData_v4 and submit that; no UserOperation is returned.
  • rail "sponsored": signed and submitted exactly like the executor rail, and ugtpFee is optional on it — when the field is absent, treat it as absent rather than as zero.
  • FEES ARE ALWAYS IN THE PUBLIC UNDERLYING, never in the private token, and ONLY A DEPOSIT AND A WITHDRAW PAY ONE: a percentage of the public amount, 0.25% by default and configurable per project, on top of that operation's network fee. The account must therefore hold the public token even when it is moving the private one.
  • Call POST /private/quote first to learn what it must hold, and treat its figures as authoritative over any rate quoted here.
  • A PRIVATE TRANSFER COSTS THE ACCOUNT NOTHING: its productFee comes back as kind "none", which is also why its amount never has to be disclosed.
  • PRIVACY: for a private transfer amountRaw MUST be omitted, and no amount is received or persisted.
  • register is a one-time, per-network account activation and it COSTS THE ACCOUNT NOTHING: it moves no asset and its productFee comes back as kind "none". There is no fee token to hold and no allowance to grant, so an account that has never deposited can register. It is the prerequisite for holding or receiving a private token, it can never be repeated for the same account, and an account that loses its key must use a fresh one.
  • 404 for an unknown private token. 400 for calldata this rail cannot relay, or an amount supplied where it must not be. 409 on a duplicate.

Authentication: X-API-Key

Input — request body

ownerAddress string required
The account the operation is for
networkId string required
Chain id
tokenSymbol string required
Private token symbol from GET /private/tokens
operationKind string required
One of private_deposit | private_transfer | private_withdraw | private_register
calls array required
Client-built encrypted-token calls ({ to, data, value })
amountRaw string
Public underlying amount in base units. Required for deposit and withdraw; MUST be omitted for a private transfer and for register.
feeTokenAddress string
Fee token override; defaults to the private token's public underlying

Input — example request

{
  "ownerAddress": "0x098afb6194f54a1ea0097fca5c9593b5ab81348e",
  "networkId": "43114",
  "tokenSymbol": "esUSDT",
  "operationKind": "private_withdraw",
  "amountRaw": "200000",
  "calls": [
    {
      "to": "0x…the encrypted-token contract",
      "data": "0x…the client's withdrawal proof",
      "value": "0"
    }
  ]
}

Output — example response (201)

{
  "operationId": "ugtp_operation_BJL0njW5DcGh",
  "rail": "forward_request",
  "requestHash": "0x141d4abead5569196b9c3165e040dab50a6a91940a1da44b779a76c5cc01307d",
  "signingPayload": {
    "domain": {
      "name": "UGTPAvalancheExecutor",
      "version": "1",
      "chainId": 43114,
      "verifyingContract": "0xA7C8776C6ac6f21eC923321793F62A4887f9E767"
    },
    "types": {
      "ExecutionRequest": [
        {
          "name": "account",
          "type": "address"
        },
        {
          "name": "nonce",
          "type": "uint256"
        },
        {
          "name": "sends",
          "type": "Send[]"
        },
        {
          "name": "swaps",
          "type": "Swap[]"
        },
        {
          "name": "bridges",
          "type": "Bridge[]"
        },
        {
          "name": "privates",
          "type": "Private[]"
        },
        {
          "name": "terms",
          "type": "FeeTerms"
        }
      ],
      "Send": [
        {
          "name": "token",
          "type": "address"
        },
        {
          "name": "recipient",
          "type": "address"
        },
        {
          "name": "amount",
          "type": "uint256"
        }
      ],
      "...": "Swap, Bridge, Private and FeeTerms members follow; the payload carries the whole tree"
    },
    "primaryType": "ExecutionRequest",
    "message": {
      "account": "0x098afb6194f54a1ea0097fca5c9593b5ab81348e",
      "nonce": "1",
      "sends": [
        {
          "token": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
          "recipient": "0x…the fee recipient",
          "amount": "500"
        }
      ],
      "swaps": [],
      "bridges": [],
      "privates": [
        "…one private action, 1280 bytes of proof and calldata"
      ],
      "terms": {
        "feeToken": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
        "transferredValueInFeeToken": "500",
        "platformTransferFeeBps": 0,
        "projectTransferFeeBps": 0,
        "platformStaticFeeUsd8dp": "5000000",
        "projectStaticFeeUsd8dp": "0",
        "projectPayoutAddress": "0x0000000000000000000000000000000000000000",
        "calldataGas": "24044",
        "maxFeeTokenAmount": "55304",
        "validUntil": 1788511916
      }
    }
  },
  "ugtpFee": {
    "tokenAmount": "55304",
    "expectedTokenAmount": "54671",
    "feeToken": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7"
  },
  "productFee": {
    "kind": "bps",
    "asset": "underlying",
    "feeRaw": "500",
    "tokenAddress": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7"
  }
}

Output — every status this endpoint answers with

StatusWhen
201Prepared private operation
400Invalid request body, the referenced account belongs to a different network than the request targets, or (7702-mode projects only) EIP-7702 is not…
401Missing or invalid credentials
404Unknown private token
409Duplicate user operation
422Blocked by security verdict
429Rate limit exceeded — retry after the number of seconds in the `Retry-After` response header
500Internal server error
Open in Swagger