# UGTP Docs — Universal Gateway Transaction Platform One integration for gasless transactions, swaps, and private tokens—built for teams that want a reliable path from intent to finality. - API base URL: https://api.ugtp.io/v1 - Swagger: https://api.ugtp.io/docs/ - OpenAPI: https://doc-dev.ugtp.io/openapi.yaml - Markdown: https://doc-dev.ugtp.io/docs.md - Authentication example: `X-API-Key: $UGTP_API_KEY` ## Ship chain-abstracted transactions without the infrastructure maze. One integration for gasless transactions, swaps, and private tokens—built for teams that want a reliable path from intent to finality. ## Start building ### [Send a gasless transfer](https://doc-dev.ugtp.io/quickstart) Most popular Prepare, sign, submit, and track a gasless transfer through one UGTP flow. ### [Understand the account model](https://doc-dev.ugtp.io/account-model) Start with identity The account is the owner EOA, activated by an EIP-7702 authorization. ### [Handle the operation rails](https://doc-dev.ugtp.io/rails) Handle every network Branch on the prepare response to sign a hash, typed data, or a transaction. ### [Build a swap](https://doc-dev.ugtp.io/swaps) Exchange assets Quote a LI.FI route, prepare it gaslessly, then sign and submit it. ## Make your first authenticated request Keep secrets server-side, verify your environment, then build the same prepare–sign–submit loop for every operation. ### 1. Authenticate Use a session JWT for onboarding and a project X-API-Key for data-plane requests. Guide: https://doc-dev.ugtp.io/authentication ### 2. Prepare and sign Prepare the operation, read its rail, and sign what that rail returned client-side. Guide: https://doc-dev.ugtp.io/rails ### 3. Submit and track Submit the signature and poll the returned operation id until terminal status. Guide: https://doc-dev.ugtp.io/lifecycle ### cURL sample ```curl curl "https://api.ugtp.io/v1/network/list" \ -H "X-API-Key: $UGTP_API_KEY" ``` ### JavaScript sample ```javascript const response = await fetch("https://api.ugtp.io/v1/network/list", { headers: { "X-API-Key": process.env.UGTP_API_KEY }, }); const networks = await response.json(); ``` ## Two credentials, two trust boundaries Session JWTs administer projects and keys. Project API keys authenticate application traffic; never place either credential in browser code or public AI prompts. ### Bearer session JWT Register, verify, or sign in, then manage projects, members, and API keys. Browse Auth endpoints →: https://doc-dev.ugtp.io/api/auth ### X-API-Key Send the project key on data-plane calls and rotate it without changing your integration flow. Manage project keys →: https://doc-dev.ugtp.io/api/api-keys ## Guides ### [Environments](https://doc-dev.ugtp.io/environments) Choose staging while validating an integration, then move the same API flow to production. Search context: Production and staging base URLs and migration guidance. Related API groups: health ### [Authentication](https://doc-dev.ugtp.io/authentication) Session JWTs manage projects and API keys; X-API-Key authenticates project data-plane calls. Search context: Bearer session JWT, project API keys, and X-API-Key usage. Related API groups: auth, api-keys ### [Account model](https://doc-dev.ugtp.io/account-model) An account is the owner EOA itself, activated per network by an EIP-7702 authorization on its first operation. Search context: EIP-7702 account model, owner EOA addresses, delegation status, and activation. Related API groups: accounts ### [Operation rails](https://doc-dev.ugtp.io/rails) The prepare response decides what the account signs: a UserOperation hash, EIP-712 typed data, or a whole transaction. Search context: Operation rails, rail discriminator, forward_request, sponsored, self_funded, and the plain rail. Related API groups: transactions ### [Avalanche C-Chain](https://doc-dev.ugtp.io/avalanche) The same API, a different mechanism: no delegation to adopt, an executor that relays what the account signed, and one allowance per token. Search context: Avalanche C-Chain, no EIP-7702, executor rail, allowance setup, native AVAX, and private tokens. Related API groups: transactions, private ### [Transactions](https://doc-dev.ugtp.io/transactions) Prepare, submit, and poll a gasless operation through terminal execution status. Search context: Gasless transfers, batched legs, submission, and operation status. Related API groups: transactions ### [Swaps](https://doc-dev.ugtp.io/swaps) Routes come from LI.FI, across the exchanges and bridges it aggregates. Quote one, then prepare it as a single gasless operation — on the same chain, or ending on another. Search context: LI.FI routing, same-chain swaps, cross-chain swaps, routes, slippage, and minimum received. Related API groups: swap ### [Private tokens](https://doc-dev.ugtp.io/private-tokens) AvaCloud's Encrypted ERC-20 (eERC) on Avalanche C-Chain: wrap a supported public token into its encrypted form, move it with a hidden amount, and unwrap it back. Registering an account and sending a private transfer both cost it nothing; only a deposit and a withdraw pay a percentage of the public amount. Search context: Private tokens, the Encrypted ERC-20 (eERC) standard by AvaCloud on Avalanche C-Chain, free registration, deposit, private transfer, and withdraw. Related API groups: private ### [Allowances](https://doc-dev.ugtp.io/allowances) The executor rail needs a one-time ERC-20 allowance per token — relayed as a permit, or funded as an ordinary approve. Search context: Executor allowance, EIP-2612 permit, funded approve, and EXECUTOR_ALLOWANCE_MISSING. Related API groups: transactions ### [Networks](https://doc-dev.ugtp.io/networks) Discover serviceable networks and supported fee tokens before preparing an operation. Search context: Network ids, chain metadata, supported fee tokens, and why the fee-token list is not the swap catalog. Related API groups: networks ### [Transaction lifecycle](https://doc-dev.ugtp.io/lifecycle#transaction-lifecycle) Track a prepared operation from signing through submission, confirmation, and finality. Search context: Operation statuses, operationId, transactionHash, replacement, retry, and finality. Related API groups: transactions ### [Status updates](https://doc-dev.ugtp.io/lifecycle#status-updates) Poll by operation id and process mirrored lifecycle fields idempotently; public webhook subscriptions are not currently available. Search context: Status payload fields, asynchronous synchronization, polling, retry, and idempotency. Related API groups: transactions ### [Fees](https://doc-dev.ugtp.io/fees) Reserve against the worst case before signing, show the expected charge, and read what settled afterwards. Search context: Fee estimate, estimatedFee, expectedFee, gasFee, serviceFee, requiredBalanceRaw, and product fees. Related API groups: fees, transactions ### [Deployed contracts](https://doc-dev.ugtp.io/contracts) Verify the contract addresses an operation touches on supported mainnet networks. Search context: Paymaster proxy, EntryPoint, account delegate, and executor addresses with block explorers. Related API groups: networks, transactions ### [Errors and recovery](https://doc-dev.ugtp.io/errors) Handle coded simulation failures, missing allowances, rate limits, duplicates, and expired preparations. Search context: SWAP_AMOUNT_TOO_SMALL, EXECUTION_SIMULATION_REVERTED, 409, 422, and 429 recovery. Related API groups: transactions, swap ## The standards this is built on Nothing here is proprietary. Every mechanism the platform uses is a public standard, which is what lets a wallet that already speaks them integrate without new cryptography. ### EIP-7702 — the account is the user's own EOA Lets an ordinary wallet address point at contract code without becoming a contract. It is why UGTP needs no second address: the user signs one authorization, their EOA adopts the account delegate, and from then on it can carry user operations. The authorization is signed per chain, which is why activation on one network says nothing about another. ### EIP-712 — typed data a wallet can display Structured data a user signs and their wallet can render as fields rather than as a hash. Avalanche has no chain-level EIP-7702, so operations there are an EIP-712 execution request the account signs with eth_signTypedData_v4 and the executor relays. It is also how a permit is signed. ### ERC-20 — the tokens being moved The token standard everything here operates on: what a transfer looks like, what an allowance is, and what decimals mean. Every amount in this API is base units of an ERC-20 — never a decimal string — which is why a token's own decimals appear in the network and token lists. ### EIP-2612 — permit, an allowance without gas An extension some ERC-20s implement that lets an allowance be granted by signature instead of by transaction. It is what allows an account holding zero native coin to authorise the executor at all. A token without it needs an ordinary approve instead, which is the other half of the allowance flow. ### eERC — the Encrypted ERC-20, for private tokens AvaCloud's standard for holding and moving an ERC-20 whose balance and transferred amount are ciphertext on chain, using zk-SNARK proofs over partially homomorphic encryption. It is the whole of the private-token feature: UGTP deploys its converter and registrar contracts on Avalanche C-Chain and relays the operations, while the client builds every proof and keeps the key that reads a balance. ### ERC-2771 — meta-transactions The pattern of a relayer submitting a transaction on an account's behalf while the contract still credits the account as the sender. The Avalanche executor grew out of it and keeps the shape — the account signs, a relayer pays — though it holds the authorization itself rather than delegating to a separate forwarder. ## A coordinated path from intent to execution UGTP keeps client signing explicit while coordinating rails, fees, routing, and network execution behind one gateway. ### Nodes - Client (client): Builds the intent, holds the account key, and signs whatever the prepare response returns. - UGTP Gateway (gateway): Authenticates public requests, resolves the project's fee terms, decides which rail carries the operation, and orchestrates the account, swap, fee, private-token, and transaction APIs. - UGTP Engine (ugtp-service): Builds the operation for its rail, relays or broadcasts it, and tracks it to a terminal status. - UGTP Paymaster (ugtp-service): Sponsors gas through EntryPoint v0.9 and collects the fee from the transferred asset on the UserOperation rail. - Avalanche executor (ugtp-service): Executes an account-signed EIP-712 request on Avalanche C-Chain and pulls its fee against a one-time allowance. - LI.FI routing (external-provider): Returns same-chain swap and cross-chain route execution data. - Chainlink price feeds (external-provider): Prices the fee's dollar-denominated parts in the fee token, read on chain by the fee-collecting contracts. - Blockchain network (network): Executes the submitted operation or the routed transfer. ### Connections - client → gateway: API request, then the signature - gateway → routing: discover a route - gateway → engine: prepare and submit - engine → paymaster: sponsor the UserOperation - engine → executor: relay the signed request - engine → network: broadcast and track - paymaster → network: execute gaslessly - executor → network: execute gaslessly - routing → network: execute the route - paymaster → price-feeds: price the fee in USD - executor → price-feeds: price the fee in USD ## Know what each service owns Use these profiles to place retries, validation, observability, and user messaging at the correct boundary. ### [LI.FI routing](https://doc-dev.ugtp.io/services#integration-lifi) Provide route discovery and execution data for UGTP same-chain swaps and cross-chain swaps and sends. Lifecycle: Swap and send quoting, route preparation, and routed transfer tracking Inputs: - Source and destination network - Input and output token - Amount and slippage Outputs: - Quote and route - Execution calldata - Routed transfer progress Trust boundary: Routing data crosses from the UGTP gateway to an external routing provider before the operation is signed or executed. Failure and degraded modes: - No serviceable route for the corridor - Route simulation rejects the amount as below a bridge or DEX minimum - The route moves between quote and prepare - The bridged transfer reaches a failed terminal state Related sections: swaps, networks, errors Operational ownership: LI.FI operates the external routing provider; UGTP owns the gateway integration and the user-facing API contract. Approved source: Repository evidence: ugtp-gateway src/services/swap.service.ts, src/services/send.service.ts and src/services/plain-swap.service.ts on origin/develop; the swap and send groups in src/content/endpoints.ts; LI.FI official documentation. Official documentation: https://docs.li.fi/ Swagger targets: - Quote a swap: https://api.ugtp.io/docs/#/swap - Prepare a swap: https://api.ugtp.io/docs/#/swap ### [UGTP Engine](https://doc-dev.ugtp.io/services#integration-engine) Build, sign for, submit, and track an operation on whichever rail carries it, behind the public gateway transaction API. Lifecycle: Preparation, submission, broadcast, and lifecycle tracking Inputs: - Owner account - Calls or typed actions - The client's signature or signed transaction Outputs: - An unsigned UserOperation, an EIP-712 request, or an unsigned transaction - The operation identifier - Lifecycle status, transaction hash, and replacement count Trust boundary: The gateway passes validated operation intent into UGTP execution services; the account's signature is produced client-side and never leaves the client unsigned. Failure and degraded modes: - Deterministic simulation revert - The prepared operation expires before submit - A duplicate operation for the same signed payload - A previous operation for the same account is still in flight Related sections: account-model, operation-rails, transactions, errors Operational ownership: UGTP operates the Engine boundary exposed through the public transaction, account, and fee APIs. Approved source: Repository evidence: ugtp-gateway src/services/operation-rail.service.ts and src/services/user-operation.service.ts on origin/develop; the accounts and transactions groups in src/content/endpoints.ts. Swagger targets: - Prepare an operation: https://api.ugtp.io/docs/#/transaction - Submit a signed operation: https://api.ugtp.io/docs/#/transaction - Read operation status: https://api.ugtp.io/docs/#/transaction ### [UGTP Paymaster](https://doc-dev.ugtp.io/services#integration-paymaster) Sponsor gas for an operation on the UserOperation rail and collect the UGTP fee from the transferred asset. Lifecycle: Fee estimation and UserOperation execution Inputs: - The UserOperation - The network - The transferred assets or the named fee token Outputs: - Gasless execution - The worst-case and expected fee in the fee token Trust boundary: The operation relies on UGTP fee policy, priced before it is signed and enforced on-chain when it executes. Failure and degraded modes: - The requested fee token is not supported on the network - The fee estimate is unavailable - The transfer leaves less behind than the fee it must pay Related sections: operation-rails, fees, deployed-contracts, errors Operational ownership: UGTP operates fee policy and the Paymaster boundary exposed by public fee and operation responses. Approved source: Repository evidence: ugtp-engine contracts/contracts/UgtpPaymasterV1.sol and api/src/features/fee/fee.service.ts on origin/develop; ugtp-gateway src/constants/ugtp-fee.ts; the fees and transactions groups in src/content/endpoints.ts. Swagger targets: - Estimate a fee: https://api.ugtp.io/docs/#/fee - Prepare a gasless transfer: https://api.ugtp.io/docs/#/transaction ### [Avalanche executor](https://doc-dev.ugtp.io/services#integration-executor) Execute an account-signed EIP-712 request on Avalanche C-Chain, where there is no chain-level EIP-7702 to delegate to. Lifecycle: Allowance setup, request preparation, and relayed execution Inputs: - The account's EIP-712 signature over the returned request - A one-time ERC-20 allowance - The fee terms the request carries Outputs: - Relayed gasless execution - The fee collected in the ERC-20 fee token Trust boundary: The account authorizes one specific request; UGTP countersigns its fee terms and relays it, and the contract refuses anything the account did not sign. Failure and degraded modes: - The account has granted no allowance for the token yet - The named fee token is not registered on the rail - The signed fee terms expire before the request is relayed - The native coin is requested, which this rail cannot move Related sections: operation-rails, allowances, fees, deployed-contracts Operational ownership: UGTP operates the executor contract, its relayer set, and the fee terms it countersigns. Approved source: Repository evidence: ugtp-engine contracts/contracts/UGTPAvalancheExecutor.sol and contracts/deployments/43114-latest.json on origin/develop; ugtp-gateway src/constants/executor-rail-networks.ts and src/services/executor-fee-terms.service.ts. Swagger targets: - Read what to sign for an allowance: https://api.ugtp.io/docs/#/transaction - Relay a permit: https://api.ugtp.io/docs/#/transaction - Submit a signed request: https://api.ugtp.io/docs/#/transaction ### [Private tokens](https://doc-dev.ugtp.io/services#integration-private-tokens) Wrap a supported public token into its encrypted form, move it with a hidden amount, and unwrap it back. The encrypted-token contracts are third-party: AvaCloud's Encrypted ERC-20 (eERC), upstream ava-labs/EncryptedERC. UGTP deploys them on Avalanche C-Chain, relays the operations and prices them. Lifecycle: Registration, deposit, private transfer, and withdraw Inputs: - The private token symbol from the token list - Client-built encrypted-token calldata - The public underlying amount, for a deposit or withdraw only Outputs: - A prepared operation on the executor rail - The product fee, charged in the public underlying - The cost quote a client can gate a Send button on Trust boundary: The client builds the encrypted-token calldata and its proof; UGTP relays it and prices the operation in the public underlying without receiving a private transfer's amount. Failure and degraded modes: - The account is not registered yet, which is a one-time prerequisite - The account holds the private token but not enough of the public underlying to pay - An amount is supplied for a private transfer, which is refused - The symbol names no active private token on the network Related sections: private-tokens, operation-rails, fees Operational ownership: UGTP operates the relaying rail and the product fee; the encrypted-token contracts are on-chain and the client owns its own proofs and viewing keys. Approved source: Repository evidence: ugtp-gateway src/routes/private.routes.ts, src/controllers/private.controller.ts, src/constants/private-tokens.ts, src/schemas/private-operation.validation-schema.ts and src/enums/operation-kind.ts on origin/develop; the private group in src/content/endpoints.ts; the vendored ava-labs/EncryptedERC contracts and AvaCloud's own documentation. Official documentation: https://docs.avacloud.io/encrypted-erc Swagger targets: - List private tokens: https://api.ugtp.io/docs/#/private - Quote a private operation: https://api.ugtp.io/docs/#/private - Prepare a private operation: https://api.ugtp.io/docs/#/private ### [EntryPoint and account delegate](https://doc-dev.ugtp.io/services#integration-account-standard) Execute user operations on chain, and provide the code an EIP-7702 account adopts. Both are canonical eth-infinitism v0.9 artifacts at the same address on every network that carries the UserOperation rail; UGTP deploys neither. Lifecycle: Account activation, and execution of every UserOperation-rail operation Inputs: - The account's EIP-7702 authorization - The signed user operation Outputs: - The delegate marker an account's code reports - On-chain execution of the operation Trust boundary: The user's own address points at third-party code once it delegates, so UGTP verifies the runtime bytecode at the delegate address against the approved implementation before building an operation for that account, and fails closed if it differs. Failure and degraded modes: - The account is delegated to a delegate that is not the approved implementation - The network has no chain-level EIP-7702, so no delegation can exist there - A validation rule of the EntryPoint rejects the operation Related sections: account-model, operation-rails, deployed-contracts Operational ownership: eth-infinitism publishes both contracts and UGTP operates neither; UGTP owns which delegate it accepts and the verification that enforces it. Approved source: Repository evidence: ugtp-engine api/src/config/eip7702.ts on origin/develop, which pins the EntryPoint v0.9 singleton, the Simple7702Account v0.9 implementation, and its approved runtime codehash. Swagger targets: - Read delegation status: https://api.ugtp.io/docs/#/account - Submit a signed operation: https://api.ugtp.io/docs/#/transaction ### [Chainlink price feeds](https://doc-dev.ugtp.io/services#integration-price-feeds) Value the dollar-denominated part of a fee in the fee token. The fee-collecting contracts read a Chainlink USD feed for the token and for the network's own coin; the gateway fetches no price itself. Lifecycle: Fee preview, and fee collection at execution Inputs: - The fee token - The network's native coin Outputs: - A USD price per unit, at the feed's own precision Trust boundary: The price is read on chain by UGTP's own contracts rather than passed in with the request, and a feed outside its configured staleness window is rejected rather than used. Failure and degraded modes: - A feed reports a stale or non-positive answer, and the operation is refused rather than priced on it - A token with no usable feed cannot be a fee token at all - The price moves between the preview and settlement, which is why the reserved figure is the worst case Related sections: fees, networks Operational ownership: Chainlink operates the feeds; UGTP owns which feed backs which token, the staleness window it must answer within, and the decision to fail closed. Approved source: Repository evidence: ugtp-engine contracts/contracts/UgtpPaymasterV1.sol and UGTPAvalancheExecutor.sol on origin/develop (IAggregatorV3.latestRoundData, StalePrice and InvalidPrice reverts), contracts/src/config/networks.ts (every supported fee token carries a mandatory USD feed), and api/src/features/fee/fee.service.ts (the preview is a read of those contracts). Official documentation: https://docs.chain.link/data-feeds Swagger targets: - Estimate a fee: https://api.ugtp.io/docs/#/fee ## Build around durable primitives The account is the owner EOA, the rail decides what gets signed, and the operation id exposes progress through finality. ## Prepare, sign, submit, observe Treat preparation as an expiring snapshot. Sign only what the rail returned, submit it, then poll the operation id until it reaches a terminal state. Prepare → Sign client-side → Submit → Track finality ## Account model An account is the owner EOA itself. There is no separate contract address to create, fund, or deploy. ### The address is the owner EOA Resolve an owner address and you get that same address back. Nothing is derived, nothing is counterfactual, and an account holds its balances at the address the user already knows. ### Activation is an EIP-7702 authorization The first operation of an unactivated account carries an authorization tuple that points the EOA at the UGTP account delegate. The prepare response says when one is needed with authorizationRequired and authorizationNonce; attach it to that submit and never to a later one. ### Activation is per network An authorization is signed for one chain, so activating on one network says nothing about another. GET /account/delegation-status reports every network separately as activated, not_activated, or unsupported. ### Avalanche C-Chain has no delegation Avalanche has no chain-level EIP-7702, so accounts there are never activated and delegation-status reports unsupported. Its operations travel the executor rail instead, and the API contract is otherwise the same. ## Operation rails One prepare, one submit — but the rail decides what the account signs. Branch on the response, not on your own network table. A prepare response without a rail field is the UserOperation rail. Every other rail names itself in rail, so treat its absence as the default and handle unknown values as an error rather than as the default. Some prepared operations answer with rail "sponsored". Sign and submit one exactly as on the executor rail, and read ugtpFee as optional on it: when the field is absent, treat it as absent rather than as zero. ### UserOperation rail - How you recognize it: no rail field - Response: Carries userOperation and userOperationHash. - Networks: Ethereum, Arbitrum One, Polygon PoS, Base, BNB Smart Chain - Assets: Any supported fee token, and the network's native coin - What the account signs: The returned userOperationHash - How you submit: POST /transaction/submit with operationId and signature, plus authorization on the first operation of an unactivated account - Gas and fee: Gasless. The Paymaster pays the network and deducts gas plus the UGTP fee from the transferred asset. ### Executor rail - How you recognize it: rail: "forward_request" - Response: Carries requestHash and signingPayload. No UserOperation is returned. - Networks: Avalanche C-Chain - Assets: ERC-20 tokens only - What the account signs: The EIP-712 signingPayload, with eth_signTypedData_v4 - How you submit: POST /transaction/submit with operationId and signature - Gas and fee: Gasless. The executor pulls gas plus the UGTP fee in an ERC-20 fee token against an allowance the account grants once. ### Self-funded rail - How you recognize it: rail: "self_funded" - Response: Carries a whole transaction, the account's on-chain nonce, and a non-binding gasSuggestion. - Networks: Avalanche C-Chain - Assets: The network's native coin, which no allowance can move - What the account signs: The whole transaction, exactly as handed over, including the returned nonce - How you submit: POST /transaction/submit with signedTransaction; send the returned operationId with it for a swap, and omit it for a transfer - Gas and fee: The account pays its own gas in the native coin. UGTP charges nothing, so no ugtpFee is returned. We broadcast and track the transaction. ### Plain rail - How you recognize it: POST /swap/plain/prepare - Response: Returns one unsigned EIP-1559 transaction; there is no userOperationHash and nothing to submit here. - Networks: Networks without chain-level EIP-7702 - Assets: Same-chain swaps only; a cross-chain request is refused - What the account signs: The returned transaction - How you submit: Nothing. The client broadcasts the transaction itself, then reports the hash to POST /transaction/register-broadcast - Gas and fee: The account pays its own gas. Without the register-broadcast call the operation never reaches a terminal status. ## Avalanche C-Chain Avalanche C-Chain reaches the same API through a different mechanism. Nothing about prepare, submit or status changes — but what the account signs does, and there is one setup step the other networks do not have. ### There is no delegation to adopt Avalanche has no chain-level EIP-7702, so an account there is never activated and GET /account/delegation-status reports it as unsupported. Do not treat that as an error or wait for an activation that cannot happen — it is the steady state for this network. ### An executor relays what the account signed Instead of a UserOperation, prepare answers rail "forward_request" with EIP-712 typed data. The account signs that with eth_signTypedData_v4, UGTP countersigns the fee terms and relays it, and the contract refuses anything the account did not sign. The published executor address is the one your operation passes through. ### Each token needs one allowance, once The executor moves tokens against an allowance, so the first gasless operation for a token is refused with EXECUTOR_ALLOWANCE_MISSING until one exists. POST /transaction/allowance-setup says whether this token takes a permit or an ordinary approve. It is per token and per account, not per operation. ### The native coin takes a third path No allowance can move AVAX, so a native operation answers rail "self_funded": the account signs the whole transaction, including the nonce it is handed, pays its own gas, and UGTP broadcasts and tracks it. There is no ugtpFee on that path, and a native leg must be the only leg. ### Private tokens live here The encrypted-token contracts are deployed on this network, so every private-token operation is an executor-rail operation. Read the available symbols from GET /private/tokens rather than assuming which network serves them. ### The fee is priced the same, collected differently The executor prices an operation identically to the Paymaster and collects it in an ERC-20 fee token, so it always needs one named — a preview without feeToken is refused with FEE_TOKEN_REQUIRED. Its terms travel with the request and are capped on-chain, so the signed maximum is a ceiling the settled charge stays under. ## Transactions One loop serves every operation on this platform: prepare it, have the account sign what the prepare answered, submit that signature, then poll the operation id until it is terminal. A transfer, a swap and a private-token operation differ only in which prepare endpoint builds them. ### Two ways to prepare, and they are not interchangeable POST /transaction/transfer takes the intent — one or more same-chain transfer legs, tokens may differ between legs — and the gateway works out how to carry it. POST /transaction/prepare takes your own call list and builds it on the UserOperation rail only. Prefer transfer for moving assets: it is the one that reaches every network. ### The rail is decided by the prepare, not chosen by you Which mechanism carries an operation follows from the network and the asset, and the response tells you by the presence of rail. That is the one branch your client needs; everything after it — submit, poll, read the fee — is the same code on every rail. ### One submit endpoint, one of two payloads POST /transaction/submit serves every rail and takes exactly one of signature or signedTransaction — a signature over what the rail returned, or the whole transaction the account signed on the self-funded rail. Re-sending the identical payload is idempotent on both shapes, so a lost response is safe to retry. ### A preparation is a snapshot with a deadline The prepared operation is held server-side for a limited time (30 minutes by default). Submit after that and it is a 404: nothing is wrong, the snapshot is simply gone, so prepare again rather than retrying the submit. A duplicate of an operation already materialized comes back 409 — read it as already booked, not as a failure. ### An account's first operation can need one extra field On the networks that delegate, prepare answers authorizationRequired and authorizationNonce for the first operation of an account that has not adopted the delegate yet — attach the EIP-7702 authorization on submit. It is never asked for again, and the rails that delegate nothing refuse the field with a 400. ## Swaps Every route here comes from LI.FI. UGTP asks it for a route across the exchanges and bridges it aggregates, then wraps that route's own calldata into a single gasless operation on whichever rail the account's network uses — so a swap is prepared, signed, submitted and polled exactly like a transfer. Where the boundary falls decides where you handle a problem. Whether a corridor is routable at all, what a route costs, and how long a bridged leg takes are the routing provider's. The fee, the signing payload, the operation id, and every status you poll are ours. Its full profile — inputs, outputs, trust boundary and failure modes — is on the Services page. ### The route is LI.FI's, the operation is UGTP's LI.FI does the route discovery and returns the execution calldata for the route it offers. UGTP checks that calldata back against the request — a route whose chain, token, recipient or amount drifted from what was asked is rejected rather than handed to an account to sign — then prices the fee, has the account sign, relays it, and tracks it to a terminal status. ### A route is chosen by price, not by speed A swap takes the cheapest of the routes the provider offers. Slippage is yours to set per request (0.005 = 0.5%, the provider's own default when omitted); nothing else about route selection is configurable through this API. ### The swap chain and token lists are the provider's catalog GET /swap/chains and GET /swap/tokens are LI.FI's own catalog, filtered to the networks this deployment serves, and each token carries LI.FI's coinKey — which is what GET /swap/token-map pairs a cross-chain corridor on. Neither is the same list as GET /network/:networkId/tokens: that one is the far shorter set of tokens an operation can pay its FEE in. ### A quote is a preview, and prepare routes again POST /swap/quote creates no operation. POST /swap/prepare fetches its OWN fresh route, because by then the one the user reviewed is minutes old — so pass reviewedMinimumReceivedRaw, the minimum the user accepted. Given it, prepare refuses with SWAP_QUOTE_MOVED rather than signing a route nobody reviewed. ### Cross-chain is the same call with a destination Give toNetworkId a different chain and the operation still runs on the source chain; the route bridges to the destination from there. So the source operation reaching a terminal status is not the same event as funds arriving on the destination chain, and how long that second leg takes belongs to the bridge on the route. ### A route can fail before anything is signed The provider having no route for a corridor and a route that exists but reverts when its calldata is simulated are different answers. The second comes back coded: SWAP_AMOUNT_TOO_SMALL when a bridge or DEX on the route rejects the amount as below its minimum, or EXECUTION_SIMULATION_REVERTED with the raw reason. Neither is fixed by retrying the identical request. ## Private tokens 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. ### Register (private_register) - What it does: Activates the account in the encrypted-token registry, which binds its address to the public key every later balance is encrypted under. - The amount: Moves no asset. amountRaw must be omitted. - What it costs: 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) - What it does: Wraps the public token into its private form. The account must already hold the public token. - The amount: amountRaw is required — this leg is public, and the amount is read back from the calldata rather than trusted from the request. - What it costs: 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) - What it does: Moves the private token to another registered account. Both the balance and the amount stay encrypted. - The amount: amountRaw MUST be omitted. The amount is never received and never persisted. - What it costs: Costs the account nothing: productFee comes back as kind "none". ### Withdraw (private_withdraw) - What it does: Unwraps the private token back into the public one, crediting the account's public balance. - The amount: amountRaw is required — the unwrapped leg is public. - What it costs: 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. ### 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. ## Allowances The executor rail moves tokens against an allowance, so an account grants one once per token before its first gasless operation there. A prepare that needs one is refused with 400 and code=EXECUTOR_ALLOWANCE_MISSING. POST /transaction/allowance-setup then answers what this token needs: kind "permit" returns EIP-712 typed data to relay through POST /transaction/setup-permit, and kind "approve" means the token has no EIP-2612 support, so the account signs an ordinary approve and UGTP broadcasts it through POST /transaction/setup-approval, which is bounded per account and per project. Neither call accepts a spender or an amount — both are decided server-side. ## Networks Two reads answer everything a prepare needs to know about a chain: where this deployment can operate, and what a fee can be charged in there. Both are served by the gateway rather than read off a chain, so treat them as the authority and fetch them instead of hardcoding a list that will drift. ### A networkId is the chain id, as a string Everywhere in this API — path, body, response — a network is its EVM chain id in decimal, as a string: "1", "137", "42161", "43114". There is no separate UGTP network name to map, and no numeric variant to convert. ### The list is what UGTP serves, not what the chain supports GET /network/list is the serviceable set behind this deployment. A chain missing from it is not a chain that is down — it is one this deployment does not carry, and no amount of retrying will prepare an operation there. ### The fee-token list is short for a reason GET /network/:networkId/tokens is what an operation can pay its FEE in on that network. A token qualifies only if the fee-collecting contracts can value it in USD from a price feed, because the fee has a dollar-denominated part. That is a property of the token, not a preference, so a token's absence is not something a request can work around — name a listed fee token instead. ### It is not the same list as the swap catalog The tokens you can SWAP are a far longer list, and they come from the routing provider rather than from us: GET /swap/tokens. A token can be swappable and still be unable to pay a fee. Read the fee list for feeToken and the swap catalog for fromToken and toToken. ### The network decides the mechanism What an account signs is a property of the network it operates on, so the same integration behaves differently across this list — and the difference is announced in the prepare response, not something you look up per chain. Avalanche C-Chain is the one that differs most. ## Transaction lifecycle Prepare once, sign what the rail asks for, then follow the operation id to a terminal state. operationId is the stable correlation key: it is what submit takes, what GET /transaction/{id} reads, and what to keep in your own records. userOperationHash is returned only on the UserOperation rail, and transactionHash is the current outer transaction hash, which can change during replacement or recovery. Submit is idempotent. If it times out before a response, it is safe to send the same signed payload again for the same operationId — and on the self-funded rail the same signed transaction bytes resolve to the operation they already booked. Do not prepare and sign a replacement while an operation is still active; keep polling the original operationId. ### Prepare Prepare the operation and read its rail. Keep the returned operationId — it is the stable identifier for every later call. A prepared operation is held server-side for a limited time; if it expires before submit, prepare again. ### Sign what the rail returns Sign the userOperationHash, the EIP-712 signingPayload, or the whole transaction, depending on the rail. Attach an EIP-7702 authorization only when the prepare response set authorizationRequired. ### Submit and track Submit the signature, then poll GET /transaction/{id} with the operationId. The transactionHash identifies the current outer transaction and can change when that transaction is replaced; replacementCount counts how often it has. ### Confirm and finalize Treat confirmed as mined and finalized as reorg-safe. Keep polling the same operationId if a reorg returns the operation to submitted. Transaction Swagger: https://api.ugtp.io/docs/#/transaction ## Status updates Use polling as the public integration contract and tolerate retried synchronization. Customer-configurable webhook subscriptions are not currently exposed. UGTP mirrors Engine lifecycle updates asynchronously, so public clients should poll GET /transaction/{id} and process status, transactionHash, replacementCount, reason, and canRetry idempotently. Operations appear from submit onward; a prepared-but-never-submitted operation returns 404. - `unknown`: No lifecycle record has been established for this operation yet. Client action: Keep polling by operationId before preparing a replacement operation. - `received`: The operation is recorded and waiting to enter the submission queue. Client action: Keep polling by operationId; do not create a replacement while it remains active. - `queued`: The operation is queued for submission. Client action: Keep polling by operationId; do not create a replacement while it remains active. - `submitted`: The operation has a current outer transactionHash and is awaiting a mined receipt. Client action: Keep polling by operationId because the current transactionHash can change during replacement. On the self-funded rail a reason of SPEED_UP_REQUIRED with canRetry means the account must re-sign the same nonce at a higher fee. - `confirmed`: The operation was mined but has not yet reached the network's reorg-safe depth. Client action: Do not retry; continue tracking until finalized if reorg safety matters. - `finalized`: The mined operation reached the network's configured reorg-safe confirmation depth. Client action: Do not retry; this is the successful reorg-safe state. - `failed`: Execution was decided by the chain and reverted, so the operation will not advance. Client action: Inspect the reported reason, correct the cause, then prepare and sign a new operation. - `dropped`: The transaction was written off as never taken by the chain. Unlike failed, this is our verdict rather than the chain's, so it can be revised if the same bytes later land. Client action: Prepare and sign a new operation if the action is still required. ### Transaction status response ```json { "operationId": "op_a1b2c3d4e5f6", "status": "confirmed", "transactionHash": "0xdef...", "userOperationHash": "0xabc...", "networkId": "137", "replacementCount": 0, "reason": null } ``` ## Fees Reserve against the worst case before signing, show the expected charge, then reconcile what the operation actually paid. ### Reserve the estimate POST /fee/estimate is a pre-execution planning value. Reserve and check sufficiency against estimatedFee, and keep that reserve outside the transfer or swap so the submitted operation can pay for itself. ### UserOperation rail The Paymaster deducts the network gas plus a $0.05 static fee plus 0.1% of the transferred input value, all denominated in the fee token. Gas is a cost rather than a rate; only the static and percentage parts are configured. ### Executor rail The executor prices an operation identically to the Paymaster and collects it in an ERC-20 fee token. Its terms travel with the request and are capped on-chain, so the signed maximum is a ceiling the settled charge stays under. ### Self-funded and plain rails The account pays its own gas in the network's native coin and UGTP charges nothing, so no ugtpFee is returned. Reserve against the estimate anyway: the balance must cover the amount plus the gas ceiling, or the prepare is refused with INSUFFICIENT_NATIVE_BALANCE. ### Product fees Swap routing and private tokens carry their own fee on top of the rail fee, resolved per project. For private tokens only a deposit and a withdraw pay one — a percentage of the PUBLIC amount, 0.25% by default — while a private transfer and a one-time registration cost the account nothing. The rate is per-project configurable, so read productFee and the quote figures the endpoint returns rather than assuming it. ### The dollar parts are priced by a Chainlink feed A $0.05 static fee and a percentage of value have to become an amount of the fee token, and that conversion happens on chain: the fee-collecting contracts read a Chainlink USD price feed for the token and for the network's own coin. A preview is a read of those same contracts, so it prices the same way — and a feed tick between the preview and settlement is one more reason estimatedFee, not expectedFee, is the figure to reserve against. ### Read the actual fee after settlement GET /transaction/history reports the fee an operation actually collected, and only once a terminal receipt has persisted it. Before settlement it can be absent. ### Fee token identity The returned symbol identifies the asset every amount in the response is denominated in. Every Raw field is that asset's base units; the paired field without the suffix is the same value formatted for display. ### estimatedFee versus expectedFee estimatedFee is the worst case — the gas limits priced at the bumped ceiling — and is the only figure to reserve, cap, or check a balance against. expectedFee is what the operation is expected to actually bill and is the one to show a user before they confirm. ### Gas and service split gasFee and serviceFee divide expectedFee into the network's own cost and the UGTP markup, and they sum back to it exactly. Display only: they arrive together or not at all, so never reserve or check sufficiency against either half. ### Required balance requiredBalanceRaw is what the account must hold of the fee token for the request to pass validation, which is not always the fee alone. It is returned on the executor rail only, and its absence must never be read as zero. ### Settled amounts After a terminal receipt, compare what the operation was quoted with what history reports it collected. A ceiling that was signed before execution is not the charge that settled. The current rail-fee configuration is a $0.05 static fee plus 0.1% of transferred input value, charged on the UserOperation and executor rails only. This is current configuration, not a permanent price guarantee, and a project can carry its own additional rates; the live estimate and the returned operation data are authoritative. Fee Swagger: https://api.ugtp.io/docs/#/fee ## Operations that ran 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. ### A gasless ERC-20 transfer - Rail: UserOperation rail (EIP-7702 account) on Arbitrum One - Operation id: `ugtp_operation_E6SnXDoAl9AQ` - What happened: 2.148465421470024202 ARB moved to 0xCa9d43184Cc3179609f77D38433D363833C73c60. The account held no ETH and paid nothing for gas. - Charged: 0.564385946233880654 ARB, deducted from the same token: 0.112759966958839437 gas + 0.449477513853571193 static + 0.002148465421470024 transfer percentage. ARB was priced at $0.11124027, which is what turns the $0.05 static fee into 0.4494 ARB. - Transaction: https://arbiscan.io/tx/0x5977159b21734798f90fad1e442216dccec579168c00354268085a6e1d608af0 #### The UserOperation the EntryPoint executed The same object as in the response, now carrying the account's signature. Its 228-byte callData is one execute(ARB, 0, transfer(recipient, amount)); the 214-byte paymasterAndData carries the paymaster, its gas limits, the fee token, the signed maximum, the transferred value, the validity window, the collection mode and the backend signature. ```json { "sender": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "nonce": "0x2", "initCode": "0x", "callData": "0xb61d27f6000000000000000000000000912ce59144191c…228 bytes", "accountGasLimits": "0x000000000000000000000000000084b800000000000000000000000000014393", "preVerificationGas": "0xc208", "gasFees": "0x00000000000000000000000000000000000000000000000000000000016eafe0", "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes", "signature": "0xa3087507768f541a4eaa9d46b581c351d29c3137…65 bytes", "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "entryPoint": "0x433709009B8330FDa32311DF1C2AFA402eD8D009", "handleOpsTransaction": "0x5977159b21734798f90fad1e442216dccec579168c00354268085a6e1d608af0" } ``` #### POST /transaction/transfer Request: ```json { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "networkId": "42161", "transfers": [ { "tokenAddress": "0x912ce59144191c1204e64559fe8253a0e49e6548", "amount": "2148465421470024202", "recipientAddress": "0xCa9d43184Cc3179609f77D38433D363833C73c60" } ] } ``` Response (201): ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "userOperation": { "sender": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "nonce": "0x2", "initCode": "0x", "callData": "0xb61d27f6000000000000000000000000912ce59144191c…228 bytes", "accountGasLimits": "0x000000000000000000000000000084b800000000000000000000000000014393", "preVerificationGas": "0xc208", "gasFees": "0x00000000000000000000000000000000000000000000000000000000016eafe0", "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes", "signature": "0x" }, "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "security": { "verdict": "safe", "reason": null, "provider": "checkpoint" }, "ugtpFee": { "tokenAmount": "622595557466841530", "feeToken": "0x912ce59144191c1204e64559fe8253a0e49e6548" }, "accountType": "eoa_7702", "authorizationRequired": false } ``` #### POST /transaction/submit Request: ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "signature": "0xa3087507768f541a4eaa9d46b581c351d29c3137…65 bytes" } ``` Response (200): ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "status": "queued", "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "networkId": "42161" } ``` #### GET /transaction/:id Response (200): ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "status": "finalized", "transactionHash": "0x5977159b21734798f90fad1e442216dccec579168c00354268085a6e1d608af0", "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "networkId": "42161", "replacementCount": 0, "reason": null } ``` ### The same transfer, where there is no EIP-7702 - Rail: Executor rail (EIP-712 execution request) on Avalanche C-Chain - Operation id: `ugtp_operation_ILmX7eF4Ngf5` - What happened: 0.2 USDt moved on Avalanche C-Chain. Same API call as the Arbitrum transfer; the response asks for typed data instead of a UserOperation hash. - Charged: 0.050351 USDT: 0.00014 gas + 0.050011 static + 0.0002 transfer percentage. The signed ceiling was 0.050908 USDT, so the charge landed under the maximum the account had agreed to. - Transaction: https://snowtrace.io/tx/0x6369d823789469741cecf701e9000710b24a3180984dc07e47fdfc277fe1a7ed #### The ExecutionRequest the executor received The account's own EIP-712 signature travels with it, and so does the backend signature that proves UGTP quoted these terms — the contract verifies both and refuses anything the account did not sign. The action stays ABI-encoded: kind 0 is a send. ```json { "account": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "nonce": "0x3", "actions": [ { "kind": 0, "params": "0x0000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4d…96 bytes" } ], "terms": { "feeToken": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7", "transferredValueInFeeToken": "200000", "platformTransferFeeBps": 10, "projectTransferFeeBps": 0, "platformStaticFeeUsd8dp": "5000000", "projectStaticFeeUsd8dp": "0", "projectPayoutAddress": "0x0000000000000000000000000000000000000000", "calldataGas": "7296", "maxFeeTokenAmount": "50908", "validUntil": 1788518426 }, "backendSignature": "0x0c9eb1440086c6f661c9c370fa25d623277990b3…65 bytes", "executor": "0xA7C8776C6ac6f21eC923321793F62A4887f9E767", "requestHash": "0x8f2f1873e757aea22e8d896fff6e6d03ef1835f922f49cb7fca6ff032fce813f", "transaction": "0x6369d823789469741cecf701e9000710b24a3180984dc07e47fdfc277fe1a7ed" } ``` #### POST /transaction/transfer Request: ```json { "ownerAddress": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "networkId": "43114", "transfers": [ { "tokenAddress": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7", "amount": "200000", "recipientAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f" } ], "feeTokenAddress": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7" } ``` Response (201): ```json { "operationId": "ugtp_operation_ILmX7eF4Ngf5", "rail": "forward_request", "requestHash": "0x8f2f1873e757aea22e8d896fff6e6d03ef1835f922f49cb7fca6ff032fce813f", "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": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "nonce": "3", "sends": [ { "token": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7", "recipient": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "amount": "200000" } ], "swaps": [], "bridges": [], "privates": [], "terms": { "feeToken": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7", "transferredValueInFeeToken": "200000", "platformTransferFeeBps": 10, "projectTransferFeeBps": 0, "platformStaticFeeUsd8dp": "5000000", "projectStaticFeeUsd8dp": "0", "projectPayoutAddress": "0x0000000000000000000000000000000000000000", "calldataGas": "7296", "maxFeeTokenAmount": "50908", "validUntil": 1788518426 } } }, "ugtpFee": { "tokenAmount": "50908", "expectedTokenAmount": "50392", "feeToken": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7" } } ``` #### GET /transaction/:id Response (200): ```json { "operationId": "ugtp_operation_ILmX7eF4Ngf5", "status": "finalized", "transactionHash": "0x6369d823789469741cecf701e9000710b24a3180984dc07e47fdfc277fe1a7ed", "userOperationHash": "0x8f2f1873e757aea22e8d896fff6e6d03ef1835f922f49cb7fca6ff032fce813f", "networkId": "43114", "replacementCount": 0, "reason": null } ``` ### The native coin, which no allowance can move - Rail: Self-funded rail (a whole transaction) on Avalanche C-Chain - Operation id: `ugtp_operation_d99WHSYNXv4j` - What happened: 0.001 AVAX moved. Nothing can relay a native balance on an account's behalf, so the account signed the whole transaction and paid its own gas — and UGTP charged nothing. - Charged: No UGTP fee at all. The operation has a fee row with every amount null, because there is nothing to collect on this rail. - Transaction: https://snowtrace.io/tx/0xd805c96742bc1aed12029fe2e1a78c487b635c0d64f94d8520a6715578f67e17 #### The signed transaction, as broadcast A plain EIP-1559 transaction — type 0x02, 117 bytes of RLP. There is no UserOperation and no executor in the picture: the operation IS this transaction, which is why its hash and the operation's own hash are the same value. ```json { "type": "0x2", "chainId": "0xa86a", "nonce": "0x4", "maxPriorityFeePerGas": "0x1", "maxFeePerGas": "0x5f9510d", "gas": "0x7b0c", "to": "0x0ea9337dc1055df7bbd16f3d4824b01c560af4c5", "value": "0x38d7ea4c68000", "data": "0x", "raw": "0x02f86f82a86a04018405f9510d827b0c940ea9337dc…117 bytes", "transactionHash": "0xd805c96742bc1aed12029fe2e1a78c487b635c0d64f94d8520a6715578f67e17" } ``` #### POST /transaction/transfer Request: ```json { "ownerAddress": "0x760ECC99d6eA52D4D209591E2eC10878cFF4e7E3", "networkId": "43114", "transfers": [ { "tokenAddress": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "amount": "1000000000000000", "recipientAddress": "0x0ea9337dc1055df7bbd16f3d4824b01c560af4c5" } ] } ``` Response (201): ```json { "rail": "self_funded", "senderAddress": "0x760ECC99d6eA52D4D209591E2eC10878cFF4e7E3", "networkId": "43114", "nonce": 4, "transaction": { "to": "0x0ea9337dc1055df7bbd16f3d4824b01c560af4c5", "data": "0x", "value": "1000000000000000" }, "gasSuggestion": { "gasLimit": "31500", "maxFeePerGas": "100000013", "maxPriorityFeePerGas": "1" } } ``` #### POST /transaction/submit Request: ```json { "signedTransaction": "0x02f86f82a86a04018405f9510d827b0c940ea9337dc…117 bytes" } ``` Response (200): ```json { "operationId": "ugtp_operation_d99WHSYNXv4j", "status": "submitted", "userOperationHash": "0xd805c96742bc1aed12029fe2e1a78c487b635c0d64f94d8520a6715578f67e17", "networkId": "43114" } ``` ### A same-chain swap on Avalanche - Rail: Executor rail (EIP-712 execution request) on Avalanche C-Chain - Operation id: `ugtp_operation_F593RTPHmsEw` - What happened: 0.3 EURC became 0.344742682283956914 DAI.e in one gasless operation, on a route LI.FI took through the OKX DEX aggregator. The account signed typed data rather than a UserOperation hash, because this network has no EIP-7702. - Charged: 0.043757 EURC: 0.000267 gas + 0.04319 static + 0.0003 transfer percentage — the static part is smaller in token units here because EURC was priced at $1.15771144. The route also carried a 20 bps LI.FI integrator fee of 0.0006 EURC. - Transaction: https://snowtrace.io/tx/0xa630f06fe8ebe731a4dbfb723f69c97e2eec29bff4d6d0157fe3f0a47db35ced #### The ExecutionRequest the executor received One action of kind 1 — a swap — whose 2944 ABI-encoded bytes hold the input token, the output token, the amount, the minimum out, and the router calldata LI.FI returned. 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. ```json { "account": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d", "nonce": "0x0", "actions": [ { "kind": 1, "params": "0x0000000000000000000000000000000000000000…2944 bytes" } ], "terms": { "feeToken": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD", "transferredValueInFeeToken": "300000", "platformTransferFeeBps": 10, "projectTransferFeeBps": 0, "platformStaticFeeUsd8dp": "5000000", "projectStaticFeeUsd8dp": "0", "projectPayoutAddress": "0x0000000000000000000000000000000000000000", "calldataGas": "27304", "maxFeeTokenAmount": "44568", "validUntil": 1788166341 }, "backendSignature": "0x33203ef5633e3a6691ea3dff38a13424c6073ed7…65 bytes", "executor": "0x…the executor deployed before 0xA7C8776C…E767", "requestHash": "0x169f5c887bfba4c7cd75d1c58ffe82dd1ee29f9f9bf675bdbcee4e4016cc6da2", "transaction": "0xa630f06fe8ebe731a4dbfb723f69c97e2eec29bff4d6d0157fe3f0a47db35ced" } ``` #### POST /swap/prepare (executor rail) Request: ```json { "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d", "networkId": "43114", "fromToken": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD", "toToken": "0xd586E7F844cEa2F87f50152665BCbc2C279D8d70", "amount": "300000", "feeToken": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD" } ``` Response (201): ```json { "operationId": "ugtp_operation_F593RTPHmsEw", "rail": "forward_request", "requestHash": "0x169f5c887bfba4c7cd75d1c58ffe82dd1ee29f9f9bf675bdbcee4e4016cc6da2", "ugtpFee": { "tokenAmount": "44568", "expectedTokenAmount": "44087", "feeToken": "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD" }, "destinationAssetSymbol": "DAI.e", "destinationAssetDecimals": 18, "destinationAmountRaw": "344742682283956914" } ``` ### A swap that ends on another chain - Rail: UserOperation rail (EIP-7702 account) on Arbitrum One - Operation id: `ugtp_operation_wdxo2cOMXBIQ` - What happened: 0.25 USDC on Arbitrum became 0.000068074903312756 ETH on Ethereum, routed by LI.FI over the Relay bridge. The transaction below is the source-chain operation; delivery on Ethereum is the bridge's own leg, and this one reported crossChainStatus "completed". - Charged: 0.072738 USDC: 0.02248 gas + 0.050008 static + 0.00025 transfer percentage. On top of it the route carried a 20 bps LI.FI integrator fee of 0.0005 USDC, recorded separately because it is not ours to keep. - Transaction: https://arbiscan.io/tx/0x1125e6fa15ea46293efa3bf06e98958b16b140d4c3f2556f13e4c1b319920a4c #### The UserOperation the EntryPoint executed One operation, two calls inside its 1924-byte callData: approve the LI.FI router for the input, then the router call that hands the amount to the bridge. Nothing about the destination chain appears in this object — it lives inside the routed calldata. ```json { "sender": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d", "nonce": "0x20", "initCode": "0x", "callData": "0xb61d27f6…1924 bytes: approve the router, then the bridge call", "accountGasLimits": "0x0000000000000000000000000000873e00000000000000000000000000043258", "preVerificationGas": "0xeeb8", "gasFees": "0x0000000000000000000000000000000000000000000000000000000001706880", "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes", "signature": "0x…65 bytes", "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8", "entryPoint": "0x433709009B8330FDa32311DF1C2AFA402eD8D009", "handleOpsTransaction": "0x1125e6fa15ea46293efa3bf06e98958b16b140d4c3f2556f13e4c1b319920a4c" } ``` #### POST /swap/prepare Request: ```json { "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d", "networkId": "42161", "toNetworkId": "1", "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "toToken": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "amount": "250000", "reviewedMinimumReceivedRaw": "67734528796192" } ``` Response (201): ```json { "operationId": "ugtp_operation_wdxo2cOMXBIQ", "userOperation": { "sender": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d", "nonce": "0x20", "initCode": "0x", "callData": "0xb61d27f6…1924 bytes: approve the router, then the bridge call", "accountGasLimits": "0x0000000000000000000000000000873e00000000000000000000000000043258", "preVerificationGas": "0xeeb8", "gasFees": "0x0000000000000000000000000000000000000000000000000000000001706880", "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes", "signature": "0x" }, "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8", "ugtpFee": { "tokenAmount": "83266", "feeToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" }, "accountType": "eoa_7702", "authorizationRequired": false } ``` #### GET /transaction/:id Response (200): ```json { "operationId": "ugtp_operation_wdxo2cOMXBIQ", "status": "finalized", "transactionHash": "0x1125e6fa15ea46293efa3bf06e98958b16b140d4c3f2556f13e4c1b319920a4c", "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8", "networkId": "42161", "replacementCount": 0, "reason": null } ``` ### Register - Rail: Sponsored lane (EIP-712 sponsored request) on Avalanche C-Chain - Operation id: `ugtp_operation_OLPsVDhbAeKF` - What happened: 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. - 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. - Transaction: https://snowtrace.io/tx/0xd90391b03b91bcfcd3dca2cb8c1cb6a7a8cfad025afc96f44e70fdd202db67f4 #### 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. ```json { "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) Request: ```json { "ownerAddress": "0x8cf41f02ea9ade6ec7bb4639fd3f13cdff6efe47", "networkId": "43114", "tokenSymbol": "esUSDT", "operationKind": "private_register", "calls": [ { "to": "0x…the registrar", "data": "0x…the client's registration proof", "value": "0" } ] } ``` Response (201): ```json { "operationId": "ugtp_operation_OLPsVDhbAeKF", "rail": "sponsored", "requestHash": "0x29694be930b9b7c3ee21f6d1d35f7a4bf63e8f6b74daedf3775e317fd84a2412", "token": { "symbol": "esUSDT", "displayName": "ES USDT", "networkId": "43114" }, "productFee": { "kind": "none", "asset": "underlying", "feeRaw": "0" } } ``` ### Deposit - Rail: Executor rail (EIP-712 execution request) on Avalanche C-Chain - Operation id: `ugtp_operation__tFU-ye2x-Y1` - What happened: 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. - 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. - Transaction: https://snowtrace.io/tx/0xb2b40e596933e5cf0ef53f97ad683abd0b34671d8fb67c2ed2f89e680decb194 #### 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. ```json { "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) Request: ```json { "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" } ] } ``` Response (201): ```json { "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" } } ``` ### Private transfer - Rail: Sponsored lane (EIP-712 sponsored request) on Avalanche C-Chain - Operation id: `ugtp_operation__jcUxmgBDnva` - What happened: A private token moved between two registered accounts. The amount is absent from the request on purpose and appears in no record afterwards. - 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. - Transaction: https://snowtrace.io/tx/0x71946a1415d8d0a9426f9d184187574422c420c9416026956cba4a3c0ac23297 #### 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. ```json { "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) Request: ```json { "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" } ] } ``` Response (201): ```json { "operationId": "ugtp_operation__jcUxmgBDnva", "rail": "sponsored", "requestHash": "0x20623579dade316efc1a739e0d5c0c8c66f5a7ed335b2434dc77dc9a1a4b4157", "token": { "symbol": "esUSDT", "displayName": "ES USDT", "networkId": "43114" }, "productFee": { "kind": "none", "asset": "underlying", "feeRaw": "0" } } ``` ### Withdraw - Rail: Executor rail (EIP-712 execution request) on Avalanche C-Chain - Operation id: `ugtp_operation_BJL0njW5DcGh` - What happened: 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. - 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. - Transaction: https://snowtrace.io/tx/0x5d0827600d4bf987ec907143c2479f1d84e49e92d26bd967411a21cb03246506 #### 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. ```json { "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 Request: ```json { "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" } ] } ``` Response (201): ```json { "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 Response (200): ```json { "operationId": "ugtp_operation_BJL0njW5DcGh", "status": "finalized", "transactionHash": "0x5d0827600d4bf987ec907143c2479f1d84e49e92d26bd967411a21cb03246506", "userOperationHash": "0x141d4abead5569196b9c3165e040dab50a6a91940a1da44b779a76c5cc01307d", "networkId": "43114", "replacementCount": 0, "reason": null } ``` ## Deployed contracts The contracts an operation touches on supported mainnets. Integrate against these addresses; the Paymaster is a proxy whose backing contract can be upgraded. ### UGTP Paymaster Sponsors gas for an operation on the UserOperation rail and collects the UGTP fee from the transferred asset. Integrate against this proxy address; the contract behind it can be upgraded. Address: `0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97` - Ethereum Mainnet (chain ID 1): `0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97` — [View on block explorer](https://etherscan.io/address/0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97) - Arbitrum One (chain ID 42161): `0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97` — [View on block explorer](https://arbiscan.io/address/0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97) - Polygon PoS (chain ID 137): `0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97` — [View on block explorer](https://polygonscan.com/address/0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97) - Base (chain ID 8453): `0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97` — [View on block explorer](https://basescan.org/address/0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97) - BNB Smart Chain (chain ID 56): `0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97` — [View on block explorer](https://bscscan.com/address/0x49f26881b5EA4d3F3d451C7faA4BCa1B4AeBCD97) ### EntryPoint v0.9 Third-party, not ours: the canonical eth-infinitism EntryPoint v0.9 singleton, which executes UserOperations on the UserOperation rail. Same address on every network that carries the rail. Address: `0x433709009B8330FDa32311DF1C2AFA402eD8D009` - Ethereum Mainnet (chain ID 1): `0x433709009B8330FDa32311DF1C2AFA402eD8D009` — [View on block explorer](https://etherscan.io/address/0x433709009B8330FDa32311DF1C2AFA402eD8D009) - Arbitrum One (chain ID 42161): `0x433709009B8330FDa32311DF1C2AFA402eD8D009` — [View on block explorer](https://arbiscan.io/address/0x433709009B8330FDa32311DF1C2AFA402eD8D009) - Polygon PoS (chain ID 137): `0x433709009B8330FDa32311DF1C2AFA402eD8D009` — [View on block explorer](https://polygonscan.com/address/0x433709009B8330FDa32311DF1C2AFA402eD8D009) - Base (chain ID 8453): `0x433709009B8330FDa32311DF1C2AFA402eD8D009` — [View on block explorer](https://basescan.org/address/0x433709009B8330FDa32311DF1C2AFA402eD8D009) - BNB Smart Chain (chain ID 56): `0x433709009B8330FDa32311DF1C2AFA402eD8D009` — [View on block explorer](https://bscscan.com/address/0x433709009B8330FDa32311DF1C2AFA402eD8D009) ### Account delegate Third-party, not ours: eth-infinitism's canonical Simple7702Account v0.9 implementation, which is the code an EIP-7702 account adopts. A delegated account's on-chain code is the 0xef0100 prefix followed by this address, which is what GET /account/delegation-status reports as the delegate. UGTP verifies the runtime bytecode at this address against the approved implementation before it builds an operation for the account. Address: `0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5` - Ethereum Mainnet (chain ID 1): `0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5` — [View on block explorer](https://etherscan.io/address/0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5) - Arbitrum One (chain ID 42161): `0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5` — [View on block explorer](https://arbiscan.io/address/0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5) - Polygon PoS (chain ID 137): `0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5` — [View on block explorer](https://polygonscan.com/address/0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5) - Base (chain ID 8453): `0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5` — [View on block explorer](https://basescan.org/address/0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5) - BNB Smart Chain (chain ID 56): `0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5` — [View on block explorer](https://bscscan.com/address/0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5) ### Avalanche executor Executes an account-signed EIP-712 execution request on Avalanche C-Chain, where EIP-7702 does not exist. It pulls the ERC-20 fee against an allowance the account granted once, so it moves nothing the account has not approved for it. Address: `0xA7C8776C6ac6f21eC923321793F62A4887f9E767` - Avalanche C-Chain (chain ID 43114): `0xA7C8776C6ac6f21eC923321793F62A4887f9E767` — [View on block explorer](https://snowtrace.io/address/0xA7C8776C6ac6f21eC923321793F62A4887f9E767) ## Errors and recovery Branch on status and stable error code, preserve the operation id in logs, and retry only when the response tells you the request is safe to repeat. ### 400 — Invalid request or missing setup Correct malformed fields, an unsupported network or token, or grant the allowance the coded reason names. ### 401 / 403 — Authentication or policy Refresh the session JWT or verify project API-key scope. ### 404 — Unknown or expired operation A prepared operation that expired, or one never submitted, is gone. Prepare again. ### 409 — Duplicate or in-flight operation Reuse the existing operation id and keep polling; a coded in-flight conflict is retryable shortly. ### 422 — Simulation rejected Inspect the coded reason, adjust the calls or the amount, and prepare again. ### 429 — Rate limited Honor Retry-After and retry with bounded exponential backoff. ### 503 — Measurement unavailable Nothing is known to be wrong — the coded reason names what could not be read. Retry. Deterministic failures: SWAP_AMOUNT_TOO_SMALL and EXECUTION_SIMULATION_REVERTED need changed input, not blind retries. EXECUTOR_ALLOWANCE_MISSING needs a one-time allowance, not a retry. ## You are viewing Production The portal, examples, and precise Swagger targets follow the selected deployment environment. Base URL: https://api.ugtp.io/v1 Production metadata and examples do not expose staging hosts. ## Ship with confidence Re-check credentials, supported networks, rail handling, status polling, fee policy, and production monitoring before you direct real user traffic to UGTP. ## API reference Curated endpoints are preserved here with request context and links to the best available Swagger tag or area. ## Health ### GET /health Health check Returns service health. No authentication required. Authentication: None Response status: 200 #### Response example ```json { "status": "ok", "timestamp": "2026-06-18T12:00:00Z", "version": "1.0.0" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/health ## Auth ### POST /auth/register Register Register a new user with name, email, and password (min 8 chars). Emails a verification code (when EMAIL_PROVIDER=console, the gateway logs the code to its console). Returns 400 if the email is from a disposable/throwaway domain ({ "error": "Disposable email addresses are not allowed" }). Returns 429 Too Many Requests if the rate limit is exceeded — this endpoint is strictly limited to prevent email bombing (check the Retry-After response header). Authentication: None #### Request body - `name` (string, required): Display name - `email` (string, required): User email address - `password` (string, required): Password, minimum 8 characters - `confirmPassword` (string, required): Must match password #### Request example ```json { "name": "Jane Dev", "email": "user@example.com", "password": "********", "confirmPassword": "********" } ``` Response status: 200 #### Response example ```json { "message": "Verification email sent", "email": "user@example.com" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/auth ### POST /auth/verify Verify email Verify the email with the 10-character code sent during registration. Returns a session JWT used for the project & API-key management endpoints. (The API key itself is minted later — see Projects & API Keys.) Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: None #### Request body - `token` (string, required): The verification code emailed at registration #### Request example ```json { "token": "A1B2C3D4E5" } ``` Response status: 200 #### Response example ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "message": "Email verified.", "systemRole": "client_admin" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/auth ### POST /auth/login Login Authenticate with email and password. Returns a session JWT (for the project & API-key management endpoints) plus the caller's platform systemRole (owner | system_admin | client_admin). Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: None #### Request body - `email` (string, required): User email - `password` (string, required): User password #### Request example ```json { "email": "user@example.com", "password": "********" } ``` Response status: 200 #### Response example ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "email": "user@example.com", "systemRole": "client_admin" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/auth ### POST /auth/resend Resend code Resend the email verification code for an unverified account. Requests are rate-limited to one per minute. Returns 429 Too Many Requests if the rate limit is exceeded — this endpoint is strictly limited to prevent email bombing (check the Retry-After response header). Authentication: None #### Request body - `email` (string, required): User email #### Request example ```json { "email": "user@example.com" } ``` Response status: 200 #### Response example ```json { "message": "New verification code sent", "email": "user@example.com" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/auth ### POST /auth/change-password Change password Change the signed-in user's password. Verifies currentPassword; newPassword must be at least 8 characters and match confirmPassword. Authentication: Bearer JWT (client session) #### Request body - `currentPassword` (string, required): Current password - `newPassword` (string, required): New password, minimum 8 characters - `confirmPassword` (string, required): Must match newPassword #### Request example ```json { "currentPassword": "********", "newPassword": "********", "confirmPassword": "********" } ``` Response status: 200 #### Response example ```json { "message": "Password updated." } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/auth ### GET /auth/current-user Current user Returns the signed-in user with their live platform systemRole (owner | system_admin | client_admin), read from the DB — so a role change is reflected on the next load without re-login. Authentication: Bearer JWT (client session) Response status: 200 #### Response example ```json { "id": "ugtp_user_a1b2c3d4e5f6", "email": "user@example.com", "name": "Jane Dev", "systemRole": "client_admin" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/auth ## Projects ### POST /project/create Create project Create a project. The authenticated user becomes its owner (only owner/admin members can mint API keys). Optionally seeds additional members by email and always auto-issues a full-access API key named after the project (raw key in apiKey.value — also persisted, so it stays retrievable later by owners/admins via GET /project/:projectId/api-key). Unknown/duplicate member emails never fail the request — they are reported in warnings instead. Authentication: Bearer JWT (client session) #### Request body - `name` (string, required): Project name - `members` (array): Optional seed members (max 20). Each item: { email: string, role?: "admin"|"member"|"viewer" (default "member") } #### Request example ```json { "name": "My App", "members": [ { "email": "teammate@example.com", "role": "admin" } ] } ``` Response status: 201 #### Response example ```json { "id": "ugtp_project_a1b2c3d4e5f6", "name": "My App", "apiKey": { "id": "ugtp_project_api_key_a1b2c3d4e5f6", "name": "My App", "keyPrefix": "ugtp2_0123", "value": "ugtp2_0123456789abcdef0123456789abcdef" }, "warnings": [] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### DELETE /project/:projectId Delete project (soft delete) Owner-only soft delete: archives the project (marks it inactive) and revokes all its still-active API keys. Nothing is removed from the database — operations, fees, status events, security reports, and memberships are retained for history/analytics. Idempotent — repeat calls still return 204. Returns 404 both if the project doesn't exist and if the caller is not its owner (non-owners get the same not-found response, never a 403). Authentication: Bearer JWT (project owner) #### Parameters - `projectId` (string, required): Project id Response status: 204 Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### GET /project/list List my projects The projects the caller belongs to, newest first. - Each item includes membersCount, counting all members, and apiKeysCount, counting active keys only. - transactionCount is the project's lifetime operation count. - The caller's own role on each project is included, so a client can gate management actions without a second call. Authentication: Bearer JWT (client session) Response status: 200 #### Response example ```json { "projects": [ { "id": "ugtp_project_a1b2c3d4e5f6", "name": "My App", "isActive": true, "role": "owner", "createdAt": "2026-06-18T12:00:00.000Z", "membersCount": 3, "apiKeysCount": 2, "lifiFeePercent": 0.2, "lifiIntegrator": "extrasafe", "eercFees": { "depositFeePercent": 0.25, "withdrawFeePercent": 0.25, "transferFlatUsd": 0.1, "depositIsDefault": true, "withdrawIsDefault": true, "transferIsDefault": true } } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### PATCH /project/:projectId/fees Set LI.FI integrator fee and eERC fee overrides Set the project's LI.FI integrator fee and/or eERC (private-token) fee overrides. All fields optional (partial update), but at least one is required. lifiFeePercent, eercDepositFeePercent, and eercWithdrawFeePercent are human-facing percents, 0–2, at most 2 decimal places (they must map exactly onto integer basis points). eercTransferFlatUsd is 0–5 dollars, in cent steps. On the eERC fields (not lifiFeePercent), null resets that field to the platform default (clears the per-project override); omitting a field leaves it untouched. Requires an owner/admin membership on the project, or platform staff. The response echoes the effective values after the write, plus isDefault flags for the eERC fields (true when the value shown is the platform default, i.e. no per-project override is set). Authentication: Bearer JWT (project owner/admin or platform staff) #### Parameters - `projectId` (string, required): Project id #### Request body - `lifiFeePercent` (number): Fee percent, 0–2, at most 2 decimal places - `eercDepositFeePercent` (number): eERC deposit fee percent, 0–2, at most 2 decimal places; null resets to the platform default - `eercWithdrawFeePercent` (number): eERC withdraw fee percent, 0–2, at most 2 decimal places; null resets to the platform default - `eercTransferFlatUsd` (number): eERC flat transfer fee in dollars, 0–5, cent steps; null resets to the platform default #### Request example ```json { "lifiFeePercent": 0.25, "eercDepositFeePercent": 0.4, "eercWithdrawFeePercent": null } ``` Response status: 200 #### Response example ```json { "projectId": "ugtp_project_a1b2c3d4e5f6", "lifiFeePercent": 0.25, "eercFees": { "depositFeePercent": 0.4, "withdrawFeePercent": 0.25, "transferFlatUsd": 0.1, "depositIsDefault": false, "withdrawIsDefault": true, "transferIsDefault": true } } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### GET /project/all List all projects (staff) Platform-staff only (owner / system_admin): every project, sorted by transaction count (desc), capped at 50, with an optional case-insensitive name search. Backs the owner/admin project picker; clients use GET /project/list (scoped to their memberships). Authentication: Bearer JWT (platform owner/system admin) #### Parameters - `search` (string): Case-insensitive project-name filter - `limit` (number): Max results (default 50, max 50) Response status: 200 #### Response example ```json { "projects": [ { "id": "ugtp_project_a1b2c3d4e5f6", "name": "Acme", "isActive": true, "transactionCount": 4210, "createdAt": "2026-06-18T12:00:00.000Z" } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### GET /project/:projectId/members/list List members List project members. Readable by any project member or platform staff. Authentication: Bearer JWT (project member or platform staff) #### Parameters - `projectId` (string, required): Project id Response status: 200 #### Response example ```json { "members": [ { "id": "ugtp_project_member_a1b2c3d4e5f6", "userId": "ugtp_user_a1b2c3d4e5f6", "email": "user@example.com", "name": "Jane Dev", "role": "owner", "createdAt": "2026-06-18T12:00:00.000Z" } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### POST /project/:projectId/members/add Add or invite member Add a project member by email. If the email belongs to an existing UGTP user they're added directly (201, { status: "added", member }). If the email is new, a pending invitation is created and emailed to the recipient (202, { status: "invited", email }) — see /project/invitations/accept. Requires an owner/admin membership on the project. Returns 404 if the project doesn't exist, 400 if the email is already a member. Authentication: Bearer JWT (project owner/admin) #### Parameters - `projectId` (string, required): Project id #### Request body - `email` (string, required): User or invitee email - `role` (string): admin | member | viewer (default member) #### Request example ```json { "email": "teammate@example.com", "role": "viewer" } ``` Response status: 202 #### Response example ```json { "added": { "status": "added", "member": { "userId": "ugtp_user_a1b2c3d4e5f6", "email": "teammate@example.com", "name": "Teammate", "role": "viewer" } }, "invited": { "status": "invited", "email": "newperson@example.com" } } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### GET /project/:projectId/invitations List pending invitations List a project's pending invitations. Requires an owner/admin membership on the project. Authentication: Bearer JWT (project owner/admin) #### Parameters - `projectId` (string, required): Project id Response status: 200 #### Response example ```json { "invitations": [ { "id": "ugtp_project_invitation_a1b2c3d4e5f6", "email": "teammate@example.com", "role": "viewer", "status": "pending", "expiresAt": "2026-07-20T12:00:00.000Z", "createdAt": "2026-07-13T12:00:00.000Z" } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### POST /project/:projectId/invitations/:invitationId/resend Resend invitation Resend the invitation email. The invitation must still be pending and belong to this project (400 otherwise). Requires an owner/admin membership on the project. Authentication: Bearer JWT (project owner/admin) #### Parameters - `projectId` (string, required): Project id - `invitationId` (string, required): Invitation id Response status: 204 Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### POST /project/:projectId/invitations/:invitationId/cancel Cancel invitation Cancel a pending invitation. The invitation must still be pending and belong to this project (400 otherwise). Requires an owner/admin membership on the project. Authentication: Bearer JWT (project owner/admin) #### Parameters - `projectId` (string, required): Project id - `invitationId` (string, required): Invitation id Response status: 204 Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### PATCH /project/:projectId/members/:memberId Change member role Set a project member's role. Requires an owner/admin membership on the project. Assigning owner requires the caller to be an owner. An owner row can only be changed by that owner themself, and only while another owner remains, so a project never ends up ownerless; a manager may change their own role. Authentication: Bearer JWT (project owner/admin) #### Parameters - `projectId` (string, required): Project id - `memberId` (string, required): Project member id #### Request body - `role` ("owner" | "admin" | "member" | "viewer", required): The role to assign #### Request example ```json { "role": "admin" } ``` Response status: 204 Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### DELETE /project/:projectId/members/:memberId Remove member Remove a non-owner project member. Requires an owner/admin membership on the project. Authentication: Bearer JWT (project owner/admin) #### Parameters - `projectId` (string, required): Project id - `memberId` (string, required): Project member id Response status: 204 Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### GET /project/invitations/accept Preview invitation Public, session-less preview of a project invitation for an accept-invitation landing page — shows who invited the recipient and to which project before they set a password. Read-only. Returns 404 if the token is unknown, 410 if it expired, 400 if token is missing. Authentication: None #### Parameters - `token` (string, required): Invitation token from the emailed link Response status: 200 #### Response example ```json { "projectName": "Acme", "email": "teammate@example.com", "role": "viewer", "expiresAt": "2026-07-20T12:00:00.000Z" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ### POST /project/invitations/accept Accept invitation Public, session-less. Consumes the invite token. For a new email it creates the account and the project membership and auto-logs in, returning a real sessionToken. For an already-existing user it attaches them to the project and answers requiresLogin: true with sessionToken: null, so the client must log in normally — auto-login off just the invite token would let anyone holding the link take over an existing account. Returns 404 if the token is unknown, 410 if it expired. Rate-limited to 5 requests/hour per IP+email — check the Retry-After response header on 429. Authentication: None #### Request body - `token` (string, required): Invitation token from the emailed link - `password` (string, required): Password, minimum 8 characters - `name` (string, required): Display name for a new account #### Request example ```json { "token": "a1b2c3d4e5f6...", "password": "********", "name": "Jane Dev" } ``` Response status: 201 #### Response example ```json { "newUser": { "requiresLogin": false, "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "projectId": "ugtp_project_a1b2c3d4e5f6", "email": "teammate@example.com" }, "existingUser": { "requiresLogin": true, "sessionToken": null, "projectId": "ugtp_project_a1b2c3d4e5f6", "email": "teammate@example.com" } } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/project ## API keys ### GET /api-key/list?projectId= List a project's API keys A project's API keys. Requires membership on the project. Each key carries a value field with the raw key string (or null if it was issued before plaintext storage was added), but only for a project owner or admin; a viewer gets every other field with value omitted entirely. Authentication: Bearer JWT (project member) #### Parameters - `projectId` (string, required): Project id (query parameter) Response status: 200 #### Response example ```json { "apiKeys": [ { "id": "ugtp_project_api_key_a1b2c3d4e5f6", "name": "Server key", "keyPrefix": "ugtp2_0123", "scopes": [ "full_access" ], "isActive": true, "expiresAt": null, "lastUsedAt": "2026-06-18T12:00:00.000Z", "createdAt": "2026-06-18T12:00:00.000Z", "revokedAt": null, "value": "ugtp2_0123456789abcdef0123456789abcdef" } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/api-key ### POST /api-key/create Create an API key Mint a project API key. The raw key is returned here and also persisted alongside the hash used for authentication, so an owner or admin can retrieve it later from GET /api-key/list. Requires an owner/admin membership on the project named in the body. A project holds at most ONE active key: while an active key exists the request fails with 409, so revoke the current key first. Authentication: Bearer JWT (project owner/admin) #### Request body - `projectId` (string, required): Project the key belongs to - `name` (string, required): Key label - `scopes` (string[], required): Key scopes; full_access is the only scope today #### Request example ```json { "projectId": "ugtp_project_a1b2c3d4e5f6", "name": "Server key", "scopes": [ "full_access" ] } ``` Response status: 201 #### Response example ```json { "id": "ugtp_project_api_key_a1b2c3d4e5f6", "apiKey": "ugtp2_0123456789abcdef0123456789abcdef", "keyPrefix": "ugtp2_0123" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/api-key ### GET /api-key/all List manageable API keys API keys the caller can manage with their owning project, newest first, capped at 100: every key for platform staff (owner / system_admin), or just the caller's owner/admin projects' keys for a project admin. Backs the cross-project key console. Authentication: Bearer JWT (platform staff or project owner/admin) Response status: 200 #### Response example ```json { "apiKeys": [ { "id": "ugtp_project_api_key_a1b2c3d4e5f6", "name": "Server key", "keyPrefix": "ugtp2_0123", "scopes": [ "full_access" ], "isActive": true, "projectId": "ugtp_project_a1b2c3d4e5f6", "projectName": "Acme", "lastUsedAt": "2026-06-18T12:00:00.000Z", "createdAt": "2026-06-18T12:00:00.000Z", "revokedAt": null, "revokedBy": null }, { "id": "ugtp_project_api_key_f6e5d4c3b2a1", "name": "Old key", "keyPrefix": "ugtp2_9876", "scopes": [ "full_access" ], "isActive": false, "projectId": "ugtp_project_a1b2c3d4e5f6", "projectName": "Acme", "lastUsedAt": null, "createdAt": "2026-06-10T12:00:00.000Z", "revokedAt": "2026-06-17T09:30:00.000Z", "revokedBy": { "name": "Ada Admin", "email": "ada@acme.com" } } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/api-key ### POST /api-key/:keyId/revoke Revoke an API key Revoke an API key by id. Platform staff may revoke any key; a project owner/admin may revoke only keys in a project they manage. Backs the cross-project key console. Authentication: Bearer JWT (platform staff or project owner/admin) #### Parameters - `keyId` (string, required): API key id Response status: 204 Swagger target (best available tag or area): https://api.ugtp.io/docs/#/api-key ## Dashboard ### GET /dashboard/stats Dashboard stats Dashboard metrics for a day, week, month or custom period, across every project the session user can access — or one project when projectId is given. - operations carries the counts: total, byStatus, and byNetwork. - fees groups platform fees by network and then by fee token: token totals, plus per-token USD from the price snapshotted on each fee, in micros where 1e6 = $1. - A separate lifi section carries the routing fee earned on swap and cross_chain_swap operations — totals, grouped by operation kind, and grouped by network and fee token. - Fee USD fields are null when no price was snapshotted for that token or operation. Authentication: Bearer JWT (project member) #### Parameters - `projectId` (string): Optional project id filter - `period` (string): day | week | month | custom (default day) - `from` (ISO datetime): Required when period=custom - `to` (ISO datetime): Range end; defaults to now - `includeTestnets` (string): Boolean-as-string, default false. Testnet networks are excluded from results unless true; an explicit networkId filter takes precedence. Response status: 200 #### Response example ```json { "period": "week", "operations": { "total": 42, "byStatus": { "confirmed": 40, "failed": 2 }, "byNetwork": { "42161": 42 } }, "fees": { "totalCollectedUsdMicros": "1250000", "totalNetUsdMicros": "900000", "byNetwork": [ { "networkId": "42161", "operations": 42, "collectedUsdMicros": "1250000", "netUsdMicros": "900000", "tokens": [ { "feeAssetId": "0xB97E...", "symbol": "USDC", "decimals": 6, "gasFeeRaw": "850000", "staticFeeRaw": "50000", "transferFeeRaw": "400000", "collectedFeeRaw": "1250000", "netRevenueRaw": "1250000", "operations": 40, "collectedUsdMicros": "1250000", "netUsdMicros": "900000" } ] } ] }, "lifi": { "totalFeeUsdMicros": "300000", "byKind": { "swap": { "operations": 5, "feeUsdMicros": "200000" }, "cross_chain_swap": { "operations": 2, "feeUsdMicros": "100000" } }, "byNetwork": [ { "networkId": "42161", "tokens": [ { "assetId": "0xB97E...", "symbol": "USDC", "decimals": 6, "feeTokenRaw": "300000", "feeUsdMicros": "300000", "operations": 7 } ] } ] } } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/dashboard ## Accounts ### POST /account/create Resolve account (derive + cache) Resolve an owner's account address on a network and cache it. Under the EIP-7702 account model the resolved address IS the owner EOA, so nothing is derived and nothing is deployed. Idempotent — re-resolving refreshes the cached details and keeps a stable id. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Request body - `ownerAddress` (string, required): Owner EOA address - `networkId` (string, required): Chain id (e.g. 43114) #### Request example ```json { "ownerAddress": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "networkId": "43114" } ``` Response status: 201 #### Response example ```json { "id": "ugtp_account_R4LJbmiFqDsJ", "address": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "networkId": "43114", "networkKind": "evm", "details": { "schemaVersion": 1, "networkKind": "evm", "evm": { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f" } }, "createdAt": "2026-06-18T12:00:00.000Z", "updatedAt": "2026-06-18T12:00:00.000Z" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ### POST /account/resolve-single Single network-independent resolve Resolve one owner EOA to its account address. Under the EIP-7702 account model that address is the owner EOA itself, so the answer is the address you sent — the call exists so an integration can resolve uniformly without special-casing the account model. Network-independent, so no networkId is needed. Stateless: it creates no account row. For many owners prefer /account/resolve-batch. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Request body - `ownerAddress` (string, required): Owner EOA address #### Request example ```json { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f" } ``` Response status: 200 #### Response example ```json { "address": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ### POST /account/info Resolve address (preview) Stateless preview of an owner's account address plus its on-chain activation flag, without persisting an account row. Under the EIP-7702 account model the address is the owner EOA and isDeployed reports whether that EOA has already adopted the UGTP delegate on this network. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Request body - `ownerAddress` (string, required): Owner EOA address - `networkId` (string, required): Chain id #### Request example ```json { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "networkId": "137" } ``` Response status: 200 #### Response example ```json { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "address": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "networkId": "137", "isDeployed": false } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ### GET /account/is-deployed Check activation on one network Whether the address is active on the given network. Under the EIP-7702 account model that means the EOA carries the delegation designator pointing at the UGTP delegate: a bare EOA, and one delegated to somebody else's contract, both report false. A network without chain-level EIP-7702 is refused with a coded 4xx rather than answered, so use GET /account/delegation-status when you want every network in one read. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `address` (string, required): Account address (the owner EOA) - `networkId` (string, required): Chain id Response status: 200 #### Response example ```json { "isDeployed": true } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ### GET /account/delegation-status EIP-7702 delegation status Whether an owner EOA has adopted the UGTP EIP-7702 delegate, network by network, read from chain state alone — no operation is built and nothing is written. There is no networkId parameter by design: an authorization is signed per chain, so the cross-network picture IS the answer. - activated — the EOA carries the delegation designator pointing at the UGTP delegate, the address echoed in delegate. - not_activated — a bare EOA, so a first operation still needs a client-signed authorization. - foreign_delegate — delegated to some other contract, which is named in delegatedTo. It is the only status that carries that field. - contract_code — the address has ordinary bytecode and is not an EOA at all. - unsupported — that chain has no chain-level EIP-7702 (Avalanche C-Chain today), so no delegation can exist there. - unavailable — that network's read failed. Deliberately never collapsed into not_activated, which would make an activated account look like it needs an authorization. - Failures are contained per network: one unreachable network degrades its own entry only, never the whole response. Every network the engine serves is reported, keyed by chain id. - A malformed address is rejected with 400. 429 when the rate limit is exceeded; check the Retry-After response header. Authentication: X-API-Key #### Parameters - `address` (string, required): Owner EOA address (0x-prefixed 20 bytes) Response status: 200 #### Response example ```json { "address": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "delegate": "0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5", "networks": { "1": { "status": "not_activated" }, "56": { "status": "not_activated" }, "137": { "status": "activated" }, "8453": { "status": "not_activated" }, "42161": { "status": "activated" }, "43114": { "status": "unsupported" }, "11155111": { "status": "activated" } } } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ### GET /account/series Account-creation series (admin chart) Account-creation time series for the admin dashboard's Accounts chart: zero-filled per-bucket NEW account counts (UTC-aligned buckets) plus a pre-window baseline per network so the client can derive a cumulative curve (cumulative = baseline + running sum). networks is ordered by in-window count, descending. Omitting bucket picks a granularity from the window length (≤31d day, ≤~6mo week, else month). Platform staff see all accounts; clients are scoped to accounts attributed (via operations) to their projects. Authentication: Bearer JWT #### Parameters - `period` (string, required): day | week | month | sixMonths | year | custom - `bucket` (string): day | week | month (default derived from window length) - `from` (string): Window start ISO date (required when period=custom) - `to` (string): Window end ISO date (default now) - `networkId` (string): Filter to one chain id - `address` (string): Exact account address (case-insensitive) - `activeWithin` (string): day | week | month | sixMonths | year — only accounts with an operation in this trailing window - `projectId` (string): Filter to accounts attributed to one project - `includeTestnets` (string): Boolean-as-string, default false. Testnet networks are excluded from results unless true; an explicit networkId filter takes precedence. Response status: 200 #### Response example ```json { "period": "month", "from": "2026-06-10T00:00:00.000Z", "to": "2026-07-10T00:00:00.000Z", "bucket": "day", "networks": [ { "networkId": "137", "name": "Polygon PoS" }, { "networkId": "42161", "name": "Arbitrum One" } ], "baseline": { "total": 7, "byNetwork": { "137": 5, "42161": 2 } }, "series": [ { "bucket": "2026-06-10T00:00:00.000Z", "total": 0, "byNetwork": {} }, { "bucket": "2026-06-11T00:00:00.000Z", "total": 3, "byNetwork": { "137": 2, "42161": 1 } } ], "total": 3 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ### POST /account/resolve-batch Batch-resolve owners Resolve many owner EOAs to their account addresses in one call. Network-independent, so only owner addresses are needed, and stateless: it creates no accounts. Under the EIP-7702 account model each smartAccountAddress equals its ownerAddress. Default cap: 1,000 owners per request — paginate for larger sets. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Request body - `ownerAddresses` (string[], required): Owner EOA addresses (1–1000 by default) #### Request example ```json { "ownerAddresses": [ "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f" ] } ``` Response status: 200 #### Response example ```json { "results": [ { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "smartAccountAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f" } ], "count": 1 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ### GET /account/list Account list (admin table) Control plane. One page of accounts, newest first, matching the filters. The page period bounds the list to accounts created in that window — the same window the account-creation series uses — so omit it for every matching account. Platform staff see all accounts; a client is scoped to accounts attributed, via their operations, to its own projects. Authentication: Bearer JWT #### Parameters - `period` (string): day | week | month | sixMonths | year | custom (from is required when custom) - `from` (string): Window start (required when period=custom) - `to` (string): Window end (defaults to now) - `networkId` (string): Filter to one chain id - `address` (string): Partial account address (case-insensitive) - `activeWithin` (string): day | week | month | sixMonths | year — only accounts with an operation in this trailing window - `projectId` (string): Filter to accounts attributed to one project - `includeTestnets` (string): Boolean-as-string, default false. Testnet networks are excluded unless true; an explicit networkId filter takes precedence. - `limit` (number): Results per page - `offset` (number): Pagination offset Response status: 200 #### Response example ```json { "accounts": [ { "id": "ugtp_account_R4LJbmiFqDsJ", "address": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "networkId": "137", "networkKind": "evm", "details": { "schemaVersion": 1, "networkKind": "evm", "evm": { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f" } }, "createdAt": "2026-06-18T12:00:00.000Z", "updatedAt": "2026-06-18T12:00:00.000Z" } ], "total": 1 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/account ## Transactions ### POST /transaction/prepare Prepare (generate user operation) Build an unsigned operation for the given calls on the UserOperation rail. transferredAssets are the asset(s) the gasless fee is charged against and they drive its percentage part. Sign the returned userOperationHash client-side, then call POST /transaction/submit. - ugtpFee is { tokenAmount, expectedTokenAmount, feeToken } in base units: tokenAmount is the worst case to reserve against, expectedTokenAmount what the operation is expected to bill. - authorizationRequired and authorizationNonce say when an EIP-7702 authorization must be attached on submit — the first operation of an account that has not adopted the delegate yet, and never a later one. - The prepared operation is held server-side for a limited time (default 30 minutes). If it expires before submit, prepare again. - 409 when the same operation was already materialized. - Coded 422 when the simulated calldata reverts deterministically: SWAP_AMOUNT_TOO_SMALL when a route rejects the amount as below its minimum, or EXECUTION_SIMULATION_REVERTED with the raw revert reason. - 404 for an unknown accountId. 400 when that account belongs to another network, or when EIP-7702 is not supported on the target network — there use /transaction/transfer or /swap/plain/prepare, which answer with the rail that does carry the operation. - 429 when the rate limit is exceeded; check the Retry-After response header. Authentication: X-API-Key #### Request body - `accountId` (string, required): Account id from POST /account/create - `networkId` (string, required): Chain id - `calls` (Call[], required): Each: { to, data?, value? } (hex) - `transferredAssets` (Asset[], required): Each: { address, amount } (base-unit hex); drives the percentage fee - `feeToken` (string): Fee token address (omit for native-gas) #### Request example ```json { "accountId": "ugtp_account_R4LJbmiFqDsJ", "networkId": "42161", "calls": [ { "to": "0x912ce59144191c1204e64559fe8253a0e49e6548", "value": "0x0", "data": "0xa9059cbb...transfer of 2148465421470024202 to 0xCa9d...3c60" } ], "transferredAssets": [ { "address": "0x912ce59144191c1204e64559fe8253a0e49e6548", "amount": "0x1dd0e1eaf2ee420a" } ] } ``` Response status: 201 #### Response example ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "userOperation": { "sender": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "nonce": "0x2", "initCode": "0x", "callData": "0xb61d27f6000000000000000000000000912ce59144191c…228 bytes", "accountGasLimits": "0x000000000000000000000000000084b800000000000000000000000000014393", "preVerificationGas": "0xc208", "gasFees": "0x00000000000000000000000000000000000000000000000000000000016eafe0", "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes", "signature": "0x" }, "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "security": { "verdict": "safe", "reason": null, "provider": "checkpoint" }, "ugtpFee": { "tokenAmount": "622595557466841530", "feeToken": "0x912ce59144191c1204e64559fe8253a0e49e6548" }, "accountType": "eoa_7702", "authorizationRequired": false } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### POST /transaction/submit Submit (send signed user operation) Submit what the client signed for a prepared operation. One endpoint serves every rail, and exactly ONE of signature or signedTransaction is required. Re-sending the same payload is idempotent on both shapes. - UserOperation rail: the signature covers the userOperationHash, and operationId is always required with it. - Executor rail, including a prepare that answered rail "sponsored": the eth_signTypedData_v4 signature over the returned signingPayload, again with operationId. - Self-funded rail: the account signs a whole transaction, so send signedTransaction instead — those bytes are the operation's identity. operationId follows what the prepare answered: present for a swap, whose router call is checked against its quote. It is absent for a native transfer, whose operation is created here and whose id comes back in the response. - authorization is EIP-7702 only and is refused with 400 on the rails that delegate nothing. - 404 when the operation is not found or expired — prepare it again. - 409 when it was already submitted, and for two coded retryable conflicts: AUTHORIZATION_IN_FLIGHT, a concurrent EIP-7702 first-operation authorization for this account, and SENDER_OPERATION_IN_FLIGHT, a previous operation that has not settled yet. - 429 when the rate limit is exceeded; check the Retry-After response header. Authentication: X-API-Key #### Request body - `operationId` (string, required): Operation id from prepare. Required with signature; on the self-funded rail send it for a swap and omit it for a native transfer. - `signature` (string): Required unless signedTransaction is sent. On the UserOperation rail, a raw secp256k1 signature of the 32-byte userOperationHash (no EIP-191 prefix). On the executor rail, and on a prepare that answered rail "sponsored", the eth_signTypedData_v4 signature over signingPayload. 0x-prefixed hex, max 4096 chars. - `signedTransaction` (string): Self-funded rail only, and mutually exclusive with signature: the account's own serialized signed EIP-1559 transaction. 0x-prefixed hex. - `authorization` (object): EIP-7702 authorization tuple, required only for the first operation of an unactivated account and refused on the other rails. All six fields are required when present: chainId (integer >= 1), address (0x-prefixed 20-byte hex), nonce (integer >= 0), yParity (0|1), r and s (0x-prefixed 32-byte hex) #### Request example ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "signature": "0xa3087507768f541a4eaa9d46b581c351d29c3137…65 bytes" } ``` Response status: 200 #### Response example ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "status": "queued", "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "networkId": "42161" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### GET /transaction/:id Get operation status Live engine status of an operation (re-polls the engine). status is the engine UserOpStatus: unknown | received | queued | submitted | confirmed | finalized | failed | dropped. finalized = execution tx reached the network's reorg-safe confirmation depth; terminal. Operations normally appear from submit onward, and a prepared-but-never-submitted operation normally returns 404. An operation may occasionally be visible with status received before submit; clients must not treat visibility as proof of submission and should continue the documented sign-and-submit flow. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `id` (string, required): Operation id Response status: 200 #### Response example ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "status": "finalized", "transactionHash": "0x5977159b21734798f90fad1e442216dccec579168c00354268085a6e1d608af0", "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "networkId": "42161", "replacementCount": 0, "reason": null } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### GET /transaction List operations List the calling project's operations, newest first, with optional account/status filters and paging. status is the last-known mirrored engine status (unknown | received | queued | submitted | confirmed | finalized | failed | dropped); call /transaction/:id for a live read. Operations normally appear after submit. An operation may occasionally be listed with status received before submit; clients must not treat listing presence as proof of submission and should continue the documented sign-and-submit flow. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `accountId` (string): Filter by account id - `status` (string): Filter by status - `limit` (number): Results per page (default 50) - `offset` (number): Pagination offset (default 0) Response status: 200 #### Response example ```json { "operations": [ { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "operationKind": "transfer", "status": "finalized", "networkId": "42161", "transactionHash": "0x5977159b21734798f90fad1e442216dccec579168c00354268085a6e1d608af0", "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "createdAt": "2026-09-02T13:56:15.463Z" } ], "count": 1, "limit": 50, "offset": 0 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### POST /transaction/transfer Prepare same-chain transfer One or more same-chain transfer legs bundled into a single gasless operation. The gateway resolves the account from ownerAddress, legs may carry different tokens, and THE RAIL IS DECIDED HERE from the network and the asset — branch on the presence of rail in the response. - No rail field: the UserOperation rail. Sign userOperationHash and submit the signature. - rail "forward_request": the executor rail. Sign the EIP-712 signingPayload with eth_signTypedData_v4 and submit that. - rail "self_funded": the native coin, which no allowance can move. The response carries a whole transaction plus the account's on-chain nonce and a non-binding gasSuggestion, and the signed bytes go to submit as signedTransaction. There is no operationId and no ugtpFee, and a native leg must be the only leg. - The prepared operation is held server-side for a limited time (default 30 minutes). If it expires before submit, prepare again. - 400 when the account belongs to another network, or when the executor rail is asked to carry the native coin. 409 on a duplicate. - Coded 422 when the operation cannot execute: INSUFFICIENT_NATIVE_BALANCE or SELF_FUNDED_NOT_EXECUTABLE on the self-funded rail, otherwise SWAP_AMOUNT_TOO_SMALL or EXECUTION_SIMULATION_REVERTED. - Coded 503 on the self-funded rail is always retryable — it names a measurement that could not be taken. - 429 when the rate limit is exceeded; check the Retry-After response header. Authentication: X-API-Key #### Request body - `ownerAddress` (string, required): Owner EOA — the gateway resolves the account from it - `networkId` (string, required): Chain id - `transfers` (Transfer[], required): Each: { tokenAddress, amount, recipientAddress }; amount = base-unit integer string; native sentinel 0xeee… for the chain coin - `feeTokenAddress` (string): Fee token (omit for native-gas) #### Request example ```json { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "networkId": "42161", "transfers": [ { "tokenAddress": "0x912ce59144191c1204e64559fe8253a0e49e6548", "amount": "2148465421470024202", "recipientAddress": "0xCa9d43184Cc3179609f77D38433D363833C73c60" } ] } ``` Response status: 201 #### Response example ```json { "operationId": "ugtp_operation_E6SnXDoAl9AQ", "userOperation": { "sender": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "nonce": "0x2", "initCode": "0x", "callData": "0xb61d27f6000000000000000000000000912ce59144191c…228 bytes", "accountGasLimits": "0x000000000000000000000000000084b800000000000000000000000000014393", "preVerificationGas": "0xc208", "gasFees": "0x00000000000000000000000000000000000000000000000000000000016eafe0", "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes", "signature": "0x" }, "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "security": { "verdict": "safe", "reason": null, "provider": "checkpoint" }, "ugtpFee": { "tokenAmount": "622595557466841530", "feeToken": "0x912ce59144191c1204e64559fe8253a0e49e6548" }, "accountType": "eoa_7702", "authorizationRequired": false } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### GET /transaction/history Account transaction history The account's own operations — sends, swaps, and cross-chain operations — newest first, each with its asset movements and the fee it actually collected. Scoped to the one account resolved from networkId plus address, and to the calling project. A pure database read: it does not re-poll the engine, fetch receipts, or query bridges, so an address with no gasless activity simply returns an empty list. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `networkId` (string, required): Chain id - `address` (string, required): Account address - `limit` (number): Results per page - `offset` (number): Pagination offset Response status: 200 #### Response example ```json { "operations": [], "count": 0, "limit": 25, "offset": 0 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### POST /transaction/allowance-setup Read what to sign for an allowance Executor rail only. Answers what THIS token needs before it can move gaslessly: kind "permit" returns EIP-712 typed data when the token implements EIP-2612, and kind "approve" names the token, spender, and amount when it does not. Read-only — it signs nothing and sends nothing. Call it after a prepare is refused with code EXECUTOR_ALLOWANCE_MISSING, then relay the signature through /transaction/setup-permit for a permit or /transaction/setup-approval for an approve. Neither the spender nor the amount is ever accepted from the caller. Returns 404 if privateTokenSymbol names no active private token. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). - Granting an allowance creates no operation: it is not returned by GET /transaction, does not appear in GET /transaction/history, and has no operationId to poll. What comes back is the on-chain transaction hash, and the allowance itself is the state to check. - This read decides nothing on its own — it reports what the token supports. The spender is never a caller-supplied address: for a private deposit it is resolved from privateTokenSymbol, and otherwise it is the executor. Authentication: X-API-Key #### Request body - `networkId` (string, required): Chain id - `ownerAddress` (string, required): The account whose allowance it is - `tokenAddress` (string, required): The ERC-20 to be granted - `privateTokenSymbol` (string): Selects a private token's own contract as the spender, which a private deposit needs; resolved from the symbol, never from a caller-supplied address #### Request example ```json { "networkId": "43114", "ownerAddress": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "tokenAddress": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7" } ``` Response status: 200 #### Response example ```json { "kind": "permit", "primaryType": "Permit", "domain": {}, "types": {}, "message": {}, "deadline": 1780000000 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### POST /transaction/setup-permit Relay an EIP-2612 permit Executor rail only, and one-time per token: relays the account's EIP-2612 permit so an account holding ZERO native coin can grant its allowance and transact gaslessly at all. The amount is never accepted from the caller — it is always the maximum — and neither is a spender address. Returns 409 with code PERMIT_ALREADY_GRANTED when this account already permitted the spender, which makes the call safely idempotent from the caller's side. Authentication: X-API-Key #### Request body - `networkId` (string, required): Chain id - `ownerAddress` (string, required): The account granting the allowance - `tokenAddress` (string, required): The ERC-20 being permitted - `deadline` (number, required): Unix timestamp from the typed data that was signed - `signature` (string, required): The account's signature over the permit typed data - `privateTokenSymbol` (string): Selects a private token's own contract as the spender instead of the executor #### Request example ```json { "networkId": "43114", "ownerAddress": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "tokenAddress": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", "deadline": 1780000000, "signature": "0x..." } ``` Response status: 200 #### Response example ```json { "transactionHash": "0x...the approve transaction UGTP broadcast", "status": "confirmed", "allowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### POST /transaction/setup-approval Broadcast the account's own approve Executor rail only, for tokens that do NOT implement EIP-2612: the account signs an ordinary approve transaction and UGTP broadcasts it, because this platform has a single broadcaster — an account left holding signed bytes has no other route to the chain. Prefer /transaction/setup-permit wherever the token supports permit. - Whether any native coin accompanies it is read from chain state and never configured: an account that can already cover its own gas is sent none, and one that falls short is topped up by the difference only. - This is ONE-TIME setup and tightly bounded — a per-account, per-token lifetime allowance plus a per-project hourly limit, both reported as 429 once spent. Build it as a step a user passes through once per token, never as something to repeat. - Deliberately minimal: the sender, token, spender, and amount are all read out of the signed bytes, and the sender is recovered from the signature, so a body naming any of them is refused. - Coded 400 when the bytes are not an acceptable approve. 403 with APPROVAL_SENDER_BLOCKED when this account previously spent a funded allowance on something else. - 409 when chain state advises against it: APPROVAL_ALREADY_GRANTED when the allowance is already in place, APPROVAL_TX_NONCE_STALE or APPROVAL_TX_NONCE_IN_FLIGHT when the signed nonce cannot mine. - 502, 503, and 504 each name a step that did not complete and whether a retry is safe. Authentication: X-API-Key #### Request body - `networkId` (string, required): Chain id - `rawTransaction` (string, required): The account's own serialized signed approve transaction, 0x-prefixed hex #### Request example ```json { "networkId": "43114", "rawTransaction": "0x02f8..." } ``` Response status: 200 #### Response example ```json { "fundingTransactionHash": "0xaaa...", "approvalTransactionHash": "0xbbb...", "approvalStatus": "confirmed", "allowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### POST /transaction/register-broadcast Report a client-broadcast transaction Plain rail only, where the CLIENT signs and broadcasts: report the hash so the operation can reach a terminal status and book its routing revenue. Without this call a plain operation never settles. The gateway never trusts the hash on its own: it is verified against the exact call the prepare recorded. A hash resolving to a different transaction is refused with 403 and TRANSACTION_MISMATCH. A second, different hash for the same operation is a 409, and a hash the node has not seen yet is a retryable 422 with TRANSACTION_NOT_VISIBLE. Returns 400 if the operation was not prepared on the plain rail, and 404 for an unknown operation, including another project's. Authentication: X-API-Key #### Request body - `operationId` (string, required): The operation the transaction belongs to - `transactionHash` (string, required): 32-byte transaction hash the client broadcast #### Request example ```json { "operationId": "ugtp_operation_d99WHSYNXv4j", "transactionHash": "0xd805c96742bc1aed12029fe2e1a78c487b635c0d64f94d8520a6715578f67e17" } ``` Response status: 200 #### Response example ```json { "operationId": "ugtp_operation_d99WHSYNXv4j", "transactionHash": "0xd805c96742bc1aed12029fe2e1a78c487b635c0d64f94d8520a6715578f67e17", "status": "submitted" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### GET /transaction/list Admin transaction list Control plane. Paginated transaction list across every project the session user can access, or one project when projectId is given, with filter-scoped revenue totals. Platform staff see all; a client is scoped to its own project memberships. - Each row carries its prepare-time security verdict (safe | unsafe | unavailable) when one was recorded. - Each row's fee object carries collectedUsdMicros, the collected amount valued in USD micros at the row's snapshotted price. netProfitUsdMicros is the collected fee minus relayer gas, both at execution-time prices. netProfitFeeRaw is that same margin in the fee token's base units, nullable and possibly negative. - Routing-fee valuation precedence: the quote-time value when the quote carried one. Else a confirmation-time snapshot, when the fee was collected in the paymaster fee token or the native coin. Else, for legacy rows only, a 1:1 conversion when the fee rode a trusted stable fee token. Else null. - A private-token row also carries productFee — our fee on that operation, always in the public underlying — with usdMicros stored at write time; legacy rows fall back to a 1:1 conversion for a $1-pegged stablecoin, else null. - totals aggregate over EVERY operation matching the current filter, not just the returned page: collectedUsdMicros, netProfitUsdMicros, lifiFeeUsdMicros, privateFeeUsdMicros, and earnedUsdMicros as their sum. - totals.tokens is a per-network, per-asset breakdown of earned revenue, sorted by earnedUsdMicros descending. The same symbol on two networks stays two separate entries. - The totals block is viewer-scoped: platform staff receive the platform figures plus projectEarnedUsdMicros, while a client receives only its own earnings. Authentication: Bearer JWT (project member) #### Parameters - `projectId` (string): Optional project id filter - `limit` (number): Results per page (default 25, max 100) - `offset` (number): Pagination offset - `status` (string): Status bucket: pending (unknown/received/queued/submitted), confirmed, finalized, failed, or dropped. A raw engine status is also accepted and matched exactly. - `networkId` (string): Exact chain id, or comma-separated chain ids - `kind` (string): Filter by operation kind: transfer | swap | cross_chain_transfer | cross_chain_swap | call | private_deposit | private_transfer | private_withdraw | private_register - `transactionHash` (string): Partial execution tx hash - `userOperationHash` (string): Partial user operation hash - `from` (ISO datetime): Created-at lower bound - `to` (ISO datetime): Created-at upper bound - `includeTestnets` (string): Boolean-as-string, default false. Testnet networks are excluded from results unless true; an explicit networkId filter takes precedence. Response status: 200 #### Response example ```json { "transactions": [ { "id": "ugtp_operation_E6SnXDoAl9AQ", "operationKind": "transfer", "status": "finalized", "networkId": "42161", "transactionHash": "0x5977159b21734798f90fad1e442216dccec579168c00354268085a6e1d608af0", "userOperationHash": "0x1a4fece09e742c04398006ebdae6d03227aa7e822ba825e0c64b44fc78d12cb3", "fee": { "symbol": "ARB", "collectedFeeRaw": "564385946233880654", "collectedUsdMicros": "62782", "netProfitUsdMicros": "50548", "netProfitFeeRaw": "454406689249794025", "lifiFeeAssetId": null, "lifiFeeAssetSymbol": null, "lifiFeeAssetDecimals": null, "lifiFeeTokenRaw": null, "lifiFeeUsdMicros": null }, "productFee": null, "security": { "verdict": "safe" }, "createdAt": "2026-09-02T13:56:15.463Z" }, { "id": "ugtp_operation_wdxo2cOMXBIQ", "operationKind": "cross_chain_swap", "status": "finalized", "networkId": "42161", "transactionHash": "0x1125e6fa15ea46293efa3bf06e98958b16b140d4c3f2556f13e4c1b319920a4c", "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8", "fee": { "symbol": "USDC", "collectedFeeRaw": "72738", "collectedUsdMicros": "72726", "netProfitUsdMicros": "54191", "netProfitFeeRaw": "54200", "lifiFeeAssetId": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "lifiFeeAssetSymbol": "USDC", "lifiFeeAssetDecimals": 6, "lifiFeeTokenRaw": "500", "lifiFeeUsdMicros": "499" }, "productFee": null, "security": { "verdict": "safe" }, "createdAt": "2026-09-04T09:34:06.000Z" }, { "id": "ugtp_operation_BJL0njW5DcGh", "operationKind": "private_withdraw", "status": "finalized", "networkId": "43114", "transactionHash": "0x5d0827600d4bf987ec907143c2479f1d84e49e92d26bd967411a21cb03246506", "userOperationHash": "0x141d4abead5569196b9c3165e040dab50a6a91940a1da44b779a76c5cc01307d", "fee": { "symbol": "USDT", "collectedFeeRaw": "54441", "collectedUsdMicros": "54430", "netProfitUsdMicros": "49987", "netProfitFeeRaw": "49997", "lifiFeeAssetId": null, "lifiFeeAssetSymbol": null, "lifiFeeAssetDecimals": null, "lifiFeeTokenRaw": null, "lifiFeeUsdMicros": null }, "productFee": { "kind": "bps", "amountRaw": "500", "symbol": "USDT", "decimals": 6, "usdMicros": "500" }, "security": { "verdict": "safe" }, "createdAt": "2026-09-04T08:46:59.706Z" } ], "total": 3, "limit": 25, "offset": 0, "totals": { "collectedUsdMicros": "110000", "netProfitUsdMicros": "81000", "lifiFeeUsdMicros": "1000000", "privateFeeUsdMicros": "5000", "earnedUsdMicros": "1086000", "tokens": [ { "networkId": "137", "assetId": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "symbol": "USDT", "decimals": 6, "earnedTokenRaw": "1023000", "earnedUsdMicros": "1023000" }, { "networkId": "42161", "assetId": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "symbol": "USDC", "decimals": 6, "earnedTokenRaw": "63000", "earnedUsdMicros": "63000" } ] } } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ### GET /transaction/series Transactions-over-time series (admin chart) Control plane. Per-bucket NEW operation counts over the requested window, zero-filled and UTC-aligned, split by network. Omitting bucket picks a granularity from the window length. Platform staff see all operations; a client is scoped to its project memberships. Authentication: Bearer JWT #### Parameters - `period` (string, required): day | week | month | custom - `bucket` (string): day | week | month (default derived from window length) - `from` (string): Window start (required when period=custom) - `to` (string): Window end (defaults to now) - `projectId` (string): Filter to one project - `includeTestnets` (string): Boolean-as-string, default false. Testnet networks are excluded unless true; an explicit networkId filter takes precedence. Response status: 200 #### Response example ```json { "period": "month", "from": "2026-06-10T00:00:00.000Z", "to": "2026-07-10T00:00:00.000Z", "bucket": "day", "networks": [ { "networkId": "137", "name": "Polygon PoS" } ], "series": [ { "bucket": "2026-06-10T00:00:00.000Z", "total": 4, "byNetwork": { "137": 4 } } ], "total": 4 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/transaction ## Swap ### GET /swap/chains List swap-supported chains LI.FI-supported chains filtered to the gateway's serviceable networks. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key Response status: 200 #### Response example ```json { "chains": [ { "networkId": "42161", "name": "Arbitrum One", "nativeCurrency": "ETH", "logoUrl": "https://..." } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/swap ### GET /swap/tokens?networkId= List swap tokens for a chain LI.FI tokens available on the given network. Each token carries LI.FI's coinKey (nullable) — the per-variant asset identifier used by /swap/token-map. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `networkId` (string, required): Chain id, e.g. 42161 Response status: 200 #### Response example ```json { "networkId": "42161", "tokens": [ { "address": "0x...", "symbol": "USDC", "name": "USD Coin", "decimals": 6, "networkId": "42161", "coinKey": "USDC" } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/swap ### GET /swap/token-map?networkId=&toNetworkId= Cross-chain same-asset token pairs for a corridor For every source-chain token with a same-asset counterpart on the destination chain, the destination token to deliver. Matched by LI.FI coinKey with same-asset family folding (USDT / USDT0 / USDt are one family), preferring the destination network's canonical token. Resolve a cross-chain send's destination token by fromAddress lookup here instead of comparing display symbols (which drift per chain). Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `networkId` (string, required): Source chain id, e.g. 42161 - `toNetworkId` (string, required): Destination chain id, e.g. 43114 (must differ) Response status: 200 #### Response example ```json { "networkId": "42161", "toNetworkId": "43114", "pairs": [ { "fromAddress": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", "toToken": { "address": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", "symbol": "USDt", "name": "TetherToken", "decimals": 6, "networkId": "43114", "coinKey": "USDt" } } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/swap ### POST /swap/quote Preview a same- or cross-chain swap Quote a swap without creating an operation. Same response shape as a cross-chain send quote, so one renderer serves both. - amountMode=exactInput (the default) treats amount as the source token to spend. exactOutput treats it as the DESTINATION target and computes the required source-side input, returned as fromAmount — which in that mode is no longer an echo of the request. - Omitting amountMode behaves exactly as exactInput, so an existing caller sees no change. - Not available on prepare: by then the caller already has a concrete fromAmount from the last resolved quote, so prepare always executes plain exact-input. - Coded 422 when the quote-time simulation reverts deterministically, for example SWAP_AMOUNT_TOO_SMALL when the route rejects the amount as below its minimum. - 429 when the rate limit is exceeded; check the Retry-After response header. Authentication: X-API-Key #### Request body - `ownerAddress` (string, required): Owner EOA — the gateway resolves the account from it - `networkId` (string, required): Source chain id (where the gasless op runs) - `toNetworkId` (string): Destination chain id (defaults to networkId; a different value makes it cross-chain) - `fromToken` (string, required): Input token address (on networkId) - `toToken` (string, required): Output token address (on toNetworkId) - `amount` (string, required): Input amount, base units - `slippage` (number): Slippage tolerance as a fraction (0.005 = 0.5%); LI.FI default when omitted #### Request example ```json { "ownerAddress": "0x9b5fcd6f647a14ebf0f88b1c3e5a3d34b951fc6f", "networkId": "137", "toNetworkId": "42161", "fromToken": "0x...", "toToken": "0x...", "amount": "1000000", "slippage": 0.005 } ``` Response status: 200 #### Response example ```json { "fromAmount": "1000000", "toAmount": "990000", "fromAmountUsd": "1.00", "toAmountUsd": "0.99", "rate": "0.99", "minimumReceived": "0.985050", "minimumReceivedUsd": "0.985", "networkFeeUsd": "0.01", "lifiFeesUsd": "0.002", "ugtpFeeUsd": "0.051", "ugtpFeeStaticUsd": 0.05, "ugtpFeePercent": 0.1, "feeTokenAmount": "0.061", "feeTokenSymbol": "USDC", "feeReserveTokenAmount": "0.061", "slippage": 0.005, "estimatedTimeSeconds": 30, "bridge": "uniswap", "provider": "uniswap" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/swap ### POST /swap/prepare Build a gasless same- or cross-chain swap user operation Wraps the routed swap as one gasless operation on the source networkId, charging the fee from fromToken or from feeToken. When toNetworkId differs it is a cross-chain swap: the operation runs on the source chain and the route bridges to the destination. Branch on the presence of rail. - No rail field: sign userOperationHash. rail "forward_request": sign the EIP-712 signingPayload and read destinationAssetSymbol, destinationAssetDecimals, and destinationAmountRaw. rail "self_funded": the account signs the whole router transaction, pays its own gas, and there is no ugtpFee. - ugtpFee carries tokenAmount, the worst case to reserve against, and expectedTokenAmount, what the operation is expected to bill. - Prepare fetches its OWN fresh route, so pass reviewedMinimumReceivedRaw — what the user accepted on the review screen. Given it, prepare refuses with SWAP_QUOTE_MOVED instead of signing a route nobody reviewed. - Submit via POST /transaction/submit, then poll GET /transaction/:id. - The prepared operation is held server-side for a limited time (default 30 minutes). If it expires before submit, prepare again. - 409 on a duplicate. Coded 422 when the simulated calldata reverts deterministically: SWAP_AMOUNT_TOO_SMALL or EXECUTION_SIMULATION_REVERTED with the raw reason. - 429 when the rate limit is exceeded; check the Retry-After response header. Authentication: X-API-Key #### Request body - `ownerAddress` (string, required): Owner EOA — the gateway resolves the account from it - `networkId` (string, required): Source chain id (where the operation runs) - `toNetworkId` (string): Destination chain id (defaults to networkId; a different value makes it cross-chain) - `fromToken` (string, required): Input token address (on networkId) - `toToken` (string, required): Output token address (on toNetworkId) - `amount` (string, required): Input amount, base units - `slippage` (number): Slippage tolerance as a fraction (0.005 = 0.5%); LI.FI default when omitted - `feeToken` (string): Fee token address (defaults to fromToken) - `reviewedMinimumReceivedRaw` (string): The minimum received the user accepted, in the destination token's base units. Given it, prepare refuses with SWAP_QUOTE_MOVED rather than signing a route the user never reviewed. #### Request example ```json { "ownerAddress": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d", "networkId": "42161", "toNetworkId": "1", "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "toToken": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "amount": "250000", "reviewedMinimumReceivedRaw": "67734528796192" } ``` Response status: 201 #### Response example ```json { "operationId": "ugtp_operation_wdxo2cOMXBIQ", "userOperation": { "sender": "0x696edf2490b92a5f5714ffbf69c1f6967cb2cd6d", "nonce": "0x20", "initCode": "0x", "callData": "0xb61d27f6…1924 bytes: approve the router, then the bridge call", "accountGasLimits": "0x0000000000000000000000000000873e00000000000000000000000000043258", "preVerificationGas": "0xeeb8", "gasFees": "0x0000000000000000000000000000000000000000000000000000000001706880", "paymasterAndData": "0x49f26881b5ea4d3f3d451c7faa4bca1b4aebcd97…214 bytes", "signature": "0x" }, "userOperationHash": "0x9f165e8e0f3957722ad6520328ce00333c29f7d26ca50401bf7a5da8540c08c8", "ugtpFee": { "tokenAmount": "83266", "feeToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" }, "accountType": "eoa_7702", "authorizationRequired": false } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/swap ### POST /swap/plain/prepare Prepare a swap the client signs and broadcasts Same-chain swap on the PLAIN rail, for a network without chain-level EIP-7702 where the gasless rail is unavailable. Returns one UNSIGNED EIP-1559 transaction the owner EOA signs and broadcasts itself, paying its own gas in the network's native coin. There is no userOperation, no userOperationHash, and nothing to submit here — after broadcasting, report the hash to POST /transaction/register-broadcast, which is the only way the operation reaches a terminal status. Cross-chain is refused rather than silently downgraded, and so is a network that does support the gasless rail. Returns a coded 422 with INSUFFICIENT_ALLOWANCE when the input token is not approved for the router. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Request body - `ownerAddress` (string, required): Owner EOA — the sender and the broadcaster - `networkId` (string, required): Chain id - `fromToken` (string, required): Input token address - `toToken` (string, required): Output token address - `amount` (string, required): Input amount, base units - `slippage` (number): Slippage tolerance as a fraction (0.005 = 0.5%) #### Request example ```json { "ownerAddress": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "networkId": "43114", "fromToken": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", "toToken": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "amount": "1000000", "slippage": 0.005 } ``` Response status: 201 #### Response example ```json { "operationId": "ugtp_operation_...", "senderAddress": "0x0a1758ad202a2059e8c2970b562597ca45d863c4", "networkId": "43114", "transaction": { "to": "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", "value": "0x0", "data": "0x..." }, "fromAmount": "1000000", "toAmount": "998500", "minimumReceived": "0.9935", "provider": "lifi" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/swap ## Networks ### GET /network/list List networks Supported networks (proxied from the engine catalog). Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key Response status: 200 #### Response example ```json { "networks": [ { "name": "Avalanche C-Chain", "networkId": "43114", "explorerUrl": "https://snowtrace.io", "nativeCoin": "AVAX", "wrappedNative": "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7" } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/network ### GET /network/:networkId Get network A single network's metadata. Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `networkId` (string, required): Chain id Response status: 200 #### Response example ```json { "name": "Avalanche C-Chain", "networkId": "43114", "explorerUrl": "https://snowtrace.io", "nativeCoin": "AVAX", "wrappedNative": "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/network ### GET /network/:networkId/tokens List network tokens Tokens supported on a network (e.g. fee tokens). Returns 429 Too Many Requests if the rate limit is exceeded (check the Retry-After response header). Authentication: X-API-Key #### Parameters - `networkId` (string, required): Chain id Response status: 200 #### Response example ```json { "networkId": "43114", "tokens": [ { "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "symbol": "USDC", "decimals": 6 } ] } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/network ## Fees ### POST /fee/estimate Estimate the fee Preview what an operation would cost for the given calls and transferred assets, without submitting anything. The shape is the same on every rail, which is what lets a wallet's Max button stay rail-agnostic. - TWO FIGURES FOR THE SAME FEE. estimatedFee and estimatedFeeRaw are the worst case: the gas limits priced at the bumped ceiling. They are the only pair to reserve, cap, or check a balance against. expectedFee and expectedFeeRaw are what the operation is expected to actually bill, and are the pair to show a user. - gasFee and serviceFee split the expected figure into the network's own cost and the UGTP markup, and sum back to it exactly. Display only, and they arrive together or not at all. - requiredBalanceRaw, on the executor rail only, is what the account must HOLD of the fee token for the request to pass validation — which is not always the fee alone. Never read its absence as zero. - On the self-funded rail nothing of ours is priced, so the whole amount is the account's own gas. - Omit feeToken for a native-coin estimate. The executor rail always charges in an ERC-20 and refuses a preview without one (FEE_TOKEN_REQUIRED); it prices ERC-20 transfers only (EXECUTOR_RAIL_PREVIEW_UNSUPPORTED, EXECUTOR_RAIL_NATIVE_UNSUPPORTED). - 429 when the rate limit is exceeded; check the Retry-After response header. Authentication: X-API-Key #### Request body - `networkId` (string, required): Chain id - `senderAddress` (string, required): The account address whose operation is priced - `calls` (Call[], required): Each: { to, data?, value? } (hex) - `transferredAssets` (Asset[], required): Each: { address, amount } (base-unit hex) - `feeToken` (string): Fee token address (omit for native-gas estimate) #### Request example ```json { "networkId": "137", "senderAddress": "0xCa9d43184Cc3179609f77D38433D363833C73c60", "calls": [ { "to": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "value": "0x0", "data": "0x..." } ], "transferredAssets": [ { "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "amount": "0xf4240" } ], "feeToken": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" } ``` Response status: 200 #### Response example ```json { "estimatedFee": "0.0512", "estimatedFeeRaw": "0xc7f8", "expectedFee": "0.0410", "expectedFeeRaw": "0xa028", "gasFee": "0.0400", "gasFeeRaw": "0x9c40", "serviceFee": "0.0010", "serviceFeeRaw": "0x3e8", "symbol": "USDT" } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/fee ## Private tokens ### GET /private/tokens List 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 #### Parameters - `networkId` (string, required): Chain ID (e.g. 43114 for Avalanche) Response status: 200 #### Response example ```json { "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 } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/private ### POST /private/quote Quote 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 #### 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. #### Request example ```json { "ownerAddress": "0x098afb6194f54a1ea0097fca5c9593b5ab81348e", "networkId": "43114", "tokenSymbol": "esUSDT", "operationKind": "private_deposit", "amountRaw": "1000000" } ``` Response status: 200 #### Response example ```json { "feeToken": { "address": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", "symbol": "USDT", "decimals": 6 }, "productFeeRaw": "2500", "networkFeeRaw": "52000", "requiredRaw": "1054500", "balanceRaw": "5000000", "sufficient": true } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/private ### POST /private/prepare Prepare 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 #### 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 #### Request example ```json { "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" } ] } ``` Response status: 201 #### Response example ```json { "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" } } ``` Swagger target (best available tag or area): https://api.ugtp.io/docs/#/private