Swagger

Private tokens

AvaCloud's Encrypted ERC-20 (eERC) on Avalanche C-Chain — wrap a supported token into its encrypted form, move it with the amount hidden, and unwrap it back.

The standard, and the four operations

A private token is a public token wrapped 1:1 into an encrypted form, where the balance and the transferred amount are ciphertext on chain. The client builds the encrypted-token calldata and its proof; UGTP relays the operation and prices it in the public token.

The encryption is not ours. It is the Encrypted ERC-20 (eERC) standard developed by AvaCloud — zk-SNARK proofs over partially homomorphic encryption, upstream ava-labs/EncryptedERC — whose EncryptedERC converter and Registrar contracts UGTP deploys, relays and prices. One converter serves every private token on the network: the underlying binds on first deposit. UGTP runs the stack on Avalanche C-Chain (chain ID 43114) and nowhere else today, which is why every private-token operation travels the executor rail. Read the available symbols from GET /private/tokens rather than assuming which network serves them.

There are four operations and they run in a fixed order: register once, then deposit to wrap, transfer privately as often as you like, and withdraw to unwrap. All four are prepared through POST /private/prepare and polled through GET /transaction/:id like any other operation.

OperationWhat it doesThe amountWhat it costs
Register
private_register
Activates the account in the encrypted-token registry, which binds its address to the public key every later balance is encrypted under.Moves no asset. amountRaw must be omitted.Costs the account nothing: productFee comes back as kind "none", and there is no fee token to hold and no allowance to grant.
Deposit
private_deposit
Wraps the public token into its private form. The account must already hold the public token.amountRaw is required — this leg is public, and the amount is read back from the calldata rather than trusted from the request.A percentage of the public amount, 0.25% by default and configurable per project, plus the operation's own network fee.
Private transfer
private_transfer
Moves the private token to another registered account. Both the balance and the amount stay encrypted.amountRaw MUST be omitted. The amount is never received and never persisted.Costs the account nothing: productFee comes back as kind "none".
Withdraw
private_withdraw
Unwraps the private token back into the public one, crediting the account's public balance.amountRaw is required — the unwrapped leg is public.A percentage of the public amount, 0.25% by default and configurable per project, plus the operation's own network fee. The fee comes out of the proceeds the withdrawal just released.

Each of these ran. The request, the response, and the object that went on chain are the operation's own, read back out of the platform's records — and the transaction link opens the result in a block explorer, so every number here can be checked against something outside this page.

Register

Sponsored lane (EIP-712 sponsored request) · Avalanche C-Chain

The one-time registration that binds an account to the public key its later private balances are encrypted under. It moves no asset, and it can never be repeated for this account.

Operation id
ugtp_operation_OLPsVDhbAeKF
Charged
Nothing. The operation has no fee row of any kind — not a network fee, not a product fee — because the platform sponsors this one's gas outright.

The SponsoredRequest the executor received

A sponsored request carries no fee terms at all — there is nothing to collect, so there is no fee token, no ceiling and no percentage. Its one private action is kind 0, registration, and its 421 bytes are the client's own proof.

{
  "account": "0x8cf41f02ea9ade6ec7bb4639fd3f13cdff6efe47",
  "nonce": "0",
  "privates": [
    {
      "kind": 0,
      "eercContract": "0x…the registrar for this network, from GET /private/tokens",
      "token": "0x0000000000000000000000000000000000000000",
      "amount": "0",
      "proofAndCalldata": "0x37729b5c2eae7e17686c53de796ee5f7675e02ea…421 bytes"
    }
  ],
  "validUntil": 1788357919,
  "backendSignature": "0x86a40e87334832ef056994f1928e7e5d004149af…65 bytes",
  "executor": "0xA7C8776C6ac6f21eC923321793F62A4887f9E767",
  "requestHash": "0x29694be930b9b7c3ee21f6d1d35f7a4bf63e8f6b74daedf3775e317fd84a2412",
  "transaction": "0xd90391b03b91bcfcd3dca2cb8c1cb6a7a8cfad025afc96f44e70fdd202db67f4"
}

POST /private/prepare (register)
{
  "ownerAddress": "0x8cf41f02ea9ade6ec7bb4639fd3f13cdff6efe47",
  "networkId": "43114",
  "tokenSymbol": "esUSDT",
  "operationKind": "private_register",
  "calls": [
    {
      "to": "0x…the registrar",
      "data": "0x…the client's registration proof",
      "value": "0"
    }
  ]
}

{
  "operationId": "ugtp_operation_OLPsVDhbAeKF",
  "rail": "sponsored",
  "requestHash": "0x29694be930b9b7c3ee21f6d1d35f7a4bf63e8f6b74daedf3775e317fd84a2412",
  "token": {
    "symbol": "esUSDT",
    "displayName": "ES USDT",
    "networkId": "43114"
  },
  "productFee": {
    "kind": "none",
    "asset": "underlying",
    "feeRaw": "0"
  }
}

Open the transaction on the block explorer ↗

Deposit

Executor rail (EIP-712 execution request) · Avalanche C-Chain

0.1 USDT wrapped into its encrypted form. This leg is public — the amount is read back out of the calldata rather than trusted from the request — which is what lets a deposit be priced as a percentage.

Operation id
ugtp_operation__tFU-ye2x-Y1
Charged
0.050497 USDT of network-and-service fee (0.000493 gas + 0.050004 static), plus a product fee of 0.00025 USDT: 0.25% of the deposited amount.

The ExecutionRequest the executor received

Three actions in one request: two private actions of kind 3 (the client's encrypted-token calls), then a send of kind 0 that pays the product fee in the public token. This one ran through the executor that preceded the published one, which is why the verifyingContract here is not the address on the contracts page.

{
  "account": "0x97514588f9ada381050763ed59109fad5d861920",
  "nonce": "0x2",
  "actions": [
    {
      "kind": 3,
      "params": "0x0000000000000000000000000000000000000000…672 bytes"
    },
    {
      "kind": 3,
      "params": "0x0000000000000000000000000000000000000000…544 bytes"
    },
    {
      "kind": 0,
      "params": "0x0000000000000000000000009702230a8ea53601…96 bytes"
    }
  ],
  "terms": {
    "feeToken": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
    "transferredValueInFeeToken": "250",
    "platformTransferFeeBps": 0,
    "projectTransferFeeBps": 0,
    "platformStaticFeeUsd8dp": "5000000",
    "projectStaticFeeUsd8dp": "0",
    "projectPayoutAddress": "0x0000000000000000000000000000000000000000",
    "calldataGas": "20284",
    "maxFeeTokenAmount": "52024",
    "validUntil": 1788169477
  },
  "backendSignature": "0xac76057c95628d50ef5a93e0562538cc0dcd1f0e…65 bytes",
  "executor": "0x…the executor deployed before 0xA7C8776C…E767",
  "requestHash": "0x91e55a96f4c8b7d75f2daa0463539df2c271fc5d76bd884606aa8bf52cb134a2",
  "transaction": "0xb2b40e596933e5cf0ef53f97ad683abd0b34671d8fb67c2ed2f89e680decb194"
}

POST /private/prepare (deposit)
{
  "ownerAddress": "0x97514588f9ada381050763ed59109fad5d861920",
  "networkId": "43114",
  "tokenSymbol": "esUSDT",
  "operationKind": "private_deposit",
  "amountRaw": "100000",
  "calls": [
    {
      "to": "0x…the encrypted-token contract",
      "data": "0x…the client's deposit proof",
      "value": "0"
    }
  ]
}

{
  "operationId": "ugtp_operation__tFU-ye2x-Y1",
  "rail": "forward_request",
  "requestHash": "0x91e55a96f4c8b7d75f2daa0463539df2c271fc5d76bd884606aa8bf52cb134a2",
  "ugtpFee": {
    "tokenAmount": "52024",
    "expectedTokenAmount": "51415",
    "feeToken": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7"
  },
  "productFee": {
    "kind": "bps",
    "asset": "underlying",
    "feeRaw": "250",
    "tokenAddress": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7"
  }
}

Open the transaction on the block explorer ↗

Private transfer

Sponsored lane (EIP-712 sponsored request) · Avalanche C-Chain

A private token moved between two registered accounts. The amount is absent from the request on purpose and appears in no record afterwards.

Operation id
ugtp_operation__jcUxmgBDnva
Charged
Nothing, again with no fee row at all. That is what lets the amount stay unknown: a charge would have to be based on it.

The SponsoredRequest the executor received

One private action of kind 2 — a transfer. Its 1572 bytes are the ciphertext and the zero-knowledge proof; `amount` is zero and `token` is the zero address, because on this path there is no public amount and no public token to name.

{
  "account": "0xe6cd7933282709399a31d035638c8bd2a805918d",
  "nonce": "0",
  "privates": [
    {
      "kind": 2,
      "eercContract": "0x…the encrypted-token contract, from GET /private/tokens",
      "token": "0x0000000000000000000000000000000000000000",
      "amount": "0",
      "proofAndCalldata": "0x2d94761000000000000000000000000071c5e529…1572 bytes"
    }
  ],
  "validUntil": 1788250577,
  "backendSignature": "0x9b4ed5f3891c7019e64476ce079f0f47ac840935…65 bytes",
  "executor": "0xA7C8776C6ac6f21eC923321793F62A4887f9E767",
  "requestHash": "0x20623579dade316efc1a739e0d5c0c8c66f5a7ed335b2434dc77dc9a1a4b4157",
  "transaction": "0x71946a1415d8d0a9426f9d184187574422c420c9416026956cba4a3c0ac23297"
}

POST /private/prepare (transfer)
{
  "ownerAddress": "0xe6cd7933282709399a31d035638c8bd2a805918d",
  "networkId": "43114",
  "tokenSymbol": "esUSDT",
  "operationKind": "private_transfer",
  "calls": [
    {
      "to": "0x…the encrypted-token contract",
      "data": "0x…the client's transfer proof",
      "value": "0"
    }
  ]
}

{
  "operationId": "ugtp_operation__jcUxmgBDnva",
  "rail": "sponsored",
  "requestHash": "0x20623579dade316efc1a739e0d5c0c8c66f5a7ed335b2434dc77dc9a1a4b4157",
  "token": {
    "symbol": "esUSDT",
    "displayName": "ES USDT",
    "networkId": "43114"
  },
  "productFee": {
    "kind": "none",
    "asset": "underlying",
    "feeRaw": "0"
  }
}

Open the transaction on the block explorer ↗

Withdraw

Executor rail (EIP-712 execution request) · Avalanche C-Chain

0.2 USDT unwrapped back out of its encrypted form. The client built the proof and the encrypted-token calldata; UGTP relayed it and priced it in the public token.

Operation id
ugtp_operation_BJL0njW5DcGh
Charged
0.054441 USDT of network-and-service fee (0.00443 gas + 0.050011 static), plus the product fee of 0.0005 USDT — 0.25% of the public amount, taken out of the proceeds the withdrawal itself released.

The ExecutionRequest the executor received

A private action of kind 3 carrying 1280 bytes of proof and calldata, then a send of kind 0 for the product fee. This one ran through the currently published executor.

{
  "account": "0x098afb6194f54a1ea0097fca5c9593b5ab81348e",
  "nonce": "0x1",
  "actions": [
    {
      "kind": 3,
      "params": "0x0000000000000000000000000000000000000000…1280 bytes"
    },
    {
      "kind": 0,
      "params": "0x0000000000000000000000009702230a8ea53601…96 bytes"
    }
  ],
  "terms": {
    "feeToken": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
    "transferredValueInFeeToken": "500",
    "platformTransferFeeBps": 0,
    "projectTransferFeeBps": 0,
    "platformStaticFeeUsd8dp": "5000000",
    "projectStaticFeeUsd8dp": "0",
    "projectPayoutAddress": "0x0000000000000000000000000000000000000000",
    "calldataGas": "24044",
    "maxFeeTokenAmount": "55304",
    "validUntil": 1788511916
  },
  "backendSignature": "0x7c2259727c9add7646104002c25689e7d5daad39…65 bytes",
  "executor": "0xA7C8776C6ac6f21eC923321793F62A4887f9E767",
  "requestHash": "0x141d4abead5569196b9c3165e040dab50a6a91940a1da44b779a76c5cc01307d",
  "transaction": "0x5d0827600d4bf987ec907143c2479f1d84e49e92d26bd967411a21cb03246506"
}

POST /private/prepare
{
  "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"
    }
  ]
}

{
  "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"
  }
}

GET /transaction/:id
{
  "operationId": "ugtp_operation_BJL0njW5DcGh",
  "status": "finalized",
  "transactionHash": "0x5d0827600d4bf987ec907143c2479f1d84e49e92d26bd967411a21cb03246506",
  "userOperationHash": "0x141d4abead5569196b9c3165e040dab50a6a91940a1da44b779a76c5cc01307d",
  "networkId": "43114",
  "replacementCount": 0,
  "reason": null
}

Open the transaction on the block explorer ↗

Registration happens once, for the life of the account

It is the prerequisite for holding or receiving a private token, and it can never be repeated for the same account — a second key would leave earlier balances encrypted under the first one and later balances under the second, and a sum of both decrypts under neither. An account that loses its key must use a fresh account.

Every fee is charged in the public token

Never in the private one. So an account can hold plenty of the private token and still be refused for want of the public one. Call POST /private/quote before the client spends the effort of building a proof: it returns what the account must hold, and requiredRaw is a ceiling that already reserves against the worst-case network fee.

Private balances carry a fixed precision

eercDecimals in the token list is the encrypted contract's own precision, not the underlying token's, and one contract serves every token on the network. A deposit converts only whole multiples of it and returns the remainder as dust — a cent on a stablecoin, a visible remainder on a high-priced underlying.

What UGTP can and cannot see

A deposit and a withdraw have a public leg, so their amounts are visible on chain by nature. A private transfer has none: its amount never reaches UGTP and is not stored, which is why the fee for it cannot be a percentage of anything.

The client owns the proofs and the viewing key

UGTP relays calldata it does not construct. Building the proof, holding the key that decrypts a balance, and deciding what to show a user all stay client-side — which is also why a lost key cannot be recovered by us.

Read the private-token service profile →

Worked example

A private deposit, end to end

Register once, learn the cost before building a proof, then prepare and submit like any other operation.

  1. 01

    Find the token

    GET /private/tokens

    Read the symbol here rather than hard-coding it. The entry also carries the encrypted-token and registrar addresses your client needs to build calldata against.

    Input — the request

    curl -X GET "https://api.ugtp.io/v1/private/tokens" \
      -H "X-API-Key: $UGTP_API_KEY"

    Output — 200 response

    {
      "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
    }

    Every status this call can answer 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
  2. 02

    Quote what the account must hold

    POST /private/quote

    Ask before the client spends the effort of a proof: both fees are taken in the PUBLIC token, so an account rich in the private one can still be refused. requiredRaw is a ceiling.

    Input — the request

    curl -X POST "https://api.ugtp.io/v1/private/quote" \
      -H "X-API-Key: $UGTP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "ownerAddress": "0x098afb6194f54a1ea0097fca5c9593b5ab81348e",
      "networkId": "43114",
      "tokenSymbol": "esUSDT",
      "operationKind": "private_deposit",
      "amountRaw": "1000000"
    }'

    Output — 200 response

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

    Every status this call can answer with
    StatusWhen
    200The operation's cost in the public underlying
    400Invalid body
    401Missing or invalid API key
    404Unknown private token for this network
  3. 03

    Prepare the operation

    POST /private/prepare

    The client builds the encrypted-token calldata; UGTP wraps and prices it. For a private transfer, omit amountRaw — the amount is never received and never stored.

    Input — the request

    curl -X POST "https://api.ugtp.io/v1/private/prepare" \
      -H "X-API-Key: $UGTP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "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 — 201 response

    {
      "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"
      }
    }

    Every status this call can answer 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