Skip to main content

Using Portals

Summary

The Portal endpoints allow you to take advantage of transaction bundling and any-to-any swaps via a simple API interface. The response includes an EIP-712 Signed Order (intent) and a valid unsigned transaction that can be submitted to a node by an externally owned account (EOA) or consumed in a smart contract. The API handles the assembly of the transaction bundle and will determine the optimal route for the exchange, ensuring the highest possible output and least gas usage, splitting the order if necessary.

The Portal transaction includes robust slippage and price impact protection mechanisms to ensure that the transaction will revert (fail) if the minOutputAmount is not satisfied at the end of the bundle. In addition, in order for the Portal transaction to succeed, all actions included in the bundle must be successful, or the transaction will revert (i.e., a Portal transaction is atomic).

You can use the estimate endpoint to quickly generate a quote for swapping an inputToken for an outputToken. The response time for this endpoint is much faster than the portal endpoint, as it does not generate a transaction bundle. This can be useful for quickly displaying the output of a Portal for informational purposes.

When you're ready to commit to a transaction, use the portal endpoint which generates a tx or signedOrder bundle that can be submitted to carry out the encoded Portal call.

Endpoints

GET v2/portal/estimate

note

This endpoint is for estimating the expected quantity of tokens that will be received following the portal transaction. This endpoint DOES NOT guarantee execution at the provided rate nor does it construct or validate a signable transaction. It is only provided for informational purposes to estimate the expected output based on current market conditions.

Example

Estimating Output for Swapping Yearn yvWETH for yvCurve-stETH Tokens

Request

Estimate a swap of 10 yearn yvWETH tokens (~19,617 USD) for Yearn yvCurve-stETH tokens:

"inputToken":"ethereum:0xa258c4606ca8206d8aa700ce2143d7db854d168c",
"inputAmount":"10000000000000000000",
"outputToken":"ethereum:0xdcd90c7f6324cfa40d7169ef80b12031770b4325",
"slippageTolerancePercentage":"0.5"

Response

{
"outputAmount": "8809924930635977077",
"minOutputAmount": "7928932437572379369",
"outputToken": "0xdcd90c7f6324cfa40d7169ef80b12031770b4325",
"outputTokenDecimals": 18
}

The estimated output is 8.81 (~$17,932 USD) of yvCurve-stETH tokens based on current market conditions. The minOutputAmount is the minimum amount of tokens that will be received if the transaction encounters slippage equal to the slippageTolerancePercentage. If the minimum amount of tokens were less than this amount, the transaction would revert (fail).

GET v2/portal

This endpoint is for generating a Portal order to transform an inputToken into an outputToken. This endpoint returns an unsigned tx object and an EIP-712 signedOrder, in addition to a context object whose properties describe the Portal order and contain other useful properties.

tip

The response will not contain a signedOrder object if the inputToken is a Native token or if the gas cost of the Portal transaction is greater than the inputAmount.

Example

Getting Curve Frax (FRAX, 3Pool) LP tokens with USDC

Request:

"inputToken": "ethereum:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"inputAmount": "28215061",
"outputToken": "ethereum:0x3175df0976dfa876431c2e9ee6bc45b65d3473cc",
"sender": "0x4689cff824d63117f9c4c42f3ec0001676f00d25",
note

Auto slippage will be used if slippageTolerancePercentage is not provided and validate is set to true (default value).

Response

The response includes a tx object which is a valid unsigned Ethereum transaction that can be signed with Ethers (or other Web3 libraries) and submitted to a node (e.g. via Metamask or XDEFI wallet). The gasLimit property includes a ~10% buffer to ensure that transaction failures due to "out of gas" errors are minimized. Due to this buffer, gas estimates will appear higher than the transaction will actually consume.

The validate flag checks that the sender has granted allowance to the target for inputToken, that they have at least inputAmount, and that they have enough native tokens to pay for the gas (if not using the signedOrder). If you would like to skip these checks, such as when the tx.data object is being consumed in a smart contract that will grant an allowance to target at execution time, set validate to false (keep in mind that auto slippage cannot be used if validate is false).

{
"tx": {
"data": "0xa2e42c650000000000000000000000000000000000000000000000000000000000000040000000000000000000000000fbd4c3d8be6b15b7cf428db2838bb44c0054fcd2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000001ae87150000000000000000000000003175df0976dfa876431c2e9ee6bc45b65d3473cc000000000000000000000000000000000000000000000001851a6aec0a8071870000000000000000000000004689cff824d63117f9c4c42f3ec0001676f00d2500000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000080ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000fbd4c3d8be6b15b7cf428db2838bb44c0054fcd20000000000000000000000000000000000000000000000000000000000014aa500000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000dcef968d416a41cdac0ed8702fac8128a64241a2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000dcef968d416a41cdac0ed8702fac8128a64241a20000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000640b4c7e4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003175df0976dfa876431c2e9ee6bc45b65d3473cc0000000000000000000000003175df0976dfa876431c2e9ee6bc45b65d3473cc000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000004689cff824d63117f9c4c42f3ec0001676f00d25000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"to": "0xbf5a7f3629fb325e2a8453d595ab103465f75e62",
"from": "0x4689cff824d63117f9c4c42f3ec0001676f00d25",
"value": "0",
"gasLimit": "337306"
},
"context": {
"orderId": "d226938a-e291-4b70-ac46-bef843657887",
"minOutputAmount": "28037839992169460103",
"minOutputAmountUsd": 28.03483837887616,
"minOutputAmountSignedOrder": "18373473875439565006",
"minOutputAmountUsdSignedOrder": 18.371506888558834,
"slippageTolerancePercentage": 0.33,
"gasLimit": "337306",
"inputAmount": "28215061",
"inputAmountUsd": 28.211844483046,
"inputToken": "ethereum:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"outputToken": "ethereum:0x3175df0976dfa876431c2e9ee6bc45b65d3473cc",
"outputAmount": "28108110267839057747",
"outputAmountUsd": 28.105101131705425,
"outputAmountSignedOrder": "18444021307341966320",
"outputAmountUsdSignedOrder": 18.44204676794972,
"partner": "0xFBD4C3D8bE6B15b7cf428Db2838bb44C0054fCd2",
"feeToken": "ethereum:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"feeAmount": "84645",
"feeAmountUsd": 0.08463535047000001,
"feeAmountSignedOrder": "9748801",
"feeAmountUsdSignedOrder": 9.747689714225704,
"sender": "0x4689cff824d63117f9c4c42f3ec0001676f00d25",
"recipient": "0x4689cff824d63117f9c4c42f3ec0001676f00d25",
"target": "ethereum:0xbf5a7f3629fb325e2a8453d595ab103465f75e62",
"value": "0",
"route": ["USDC", "crvFRAX"],
"steps": [
"Transfer 0.084645 USDC to fee collector at ethereum:0xFBD4C3D8bE6B15b7cf428Db2838bb44C0054fCd2",
"Approve USDC to 0xdcef968d416a41cdac0ed8702fac8128a64241a2",
"Curve add_liquidity from USDC to crvFRAX",
"Transfer crvFRAX to 0x4689cff824d63117f9c4c42f3ec0001676f00d25"
],
"stepsSignedOrder": [
"Transfer 9.748801 USDC to fee collector at ethereum:0xFBD4C3D8bE6B15b7cf428Db2838bb44C0054fCd2",
"Approve USDC to 0xdcef968d416a41cdac0ed8702fac8128a64241a2",
"Curve add_liquidity from USDC to crvFRAX",
"Transfer crvFRAX to 0x4689cff824d63117f9c4c42f3ec0001676f00d25"
],
"routeHash": "0xd67340dad900bb9489ec3aabdf9cdbb0309988dc0279f44dc03d3d94bfb4cf3a"
},
"signedOrder": {
"types": {
"Order": [
{
"name": "inputToken",
"type": "address"
},
{
"name": "inputAmount",
"type": "uint256"
},
{
"name": "outputToken",
"type": "address"
},
{
"name": "minOutputAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
],
"SignedOrder": [
{
"name": "order",
"type": "Order"
},
{
"name": "routeHash",
"type": "bytes32"
},
{
"name": "sender",
"type": "address"
},
{
"name": "deadline",
"type": "uint64"
},
{
"name": "nonce",
"type": "uint64"
}
]
},
"domain": {
"name": "PortalsRouter",
"version": "1",
"chainId": 1,
"verifyingContract": "0xbf5a7f3629fb325e2a8453d595ab103465f75e62"
},
"value": {
"order": {
"inputToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"outputToken": "0x3175df0976dfa876431c2e9ee6bc45b65d3473cc",
"inputAmount": "28215061",
"minOutputAmount": "18373473875439565006",
"recipient": "0x4689cff824d63117f9c4c42f3ec0001676f00d25"
},
"routeHash": "0x47bd3e254ee6bbdfe9053cd260d8073c18c126bcbd91b6d82d663d60bc6ae98b",
"sender": "0x4689cff824d63117f9c4c42f3ec0001676f00d25",
"deadline": "1690366882",
"nonce": "0"
}
}
}

Submitting orders with gasless approvals (permit)

If you are making use of gasless approvals via permit (as described here), you can submit the permit signature as part of the request with the permitSignature param. The signature is an EIP-712 secp256k1 signature of bytes type and is defined in EIP-2612.

note

You can use gasless approvals without using gasless transactions (i.e., you can bundle the approval and value transaction together but still submit it yourself and pay the gas).

Troubleshooting

The endpoints test the validity of the transaction (when validate = true) and return any relevant errors back to you. Some of the error types are:

  • The calculated slippage is higher than the threshold set by the slippageTolerancePercentage which would cause the transaction to revert.
    • Try increasing the slippageTolerancePercentage which is the cumulative maximum acceptable slippage for the entire batch of transactions
  • The sender has an insufficient balance of inputToken.
    • The sender does not have enough tokens for the transaction. Reduce the inputToken or double check the sender's balance with the Account endpoint.
  • The sender does not have enough of the native token to pay for gas
    • The sender needs native tokens to pay for the gas used by the transaction. If you have used the maximum native token balance of sender (e.g. by using a MAX button on an input), ensure some is reserved to cover the gas.
  • The sender has not granted approval for the portal contract (or the permit signature is invalid)
    • The sender needs to grant approval to the target to spend inputToken. Use the Approval endpoint to generate an approval transaction and check allowances.
  • There isn't enough liquidity to complete the swap
    • Portals is unable to find a route with sufficient liquidity to complete the swap. Try the request later or reach out to the Portals team and let us know about the issue.
  • Auto slippage exceeds 2.5%. Expected slippage is x%. Set slippage manually to override.
    • Portals Auto Slippage and price impact tolerance is exceeded for this swap or zap. Set slippageTolerancePercentage manually to proceed with the transaction. Portals will still validate that the swap falls within your specified slippage tolerance, but proceed with caution!
  • A signedOrder object is not present in the response

POST v2/portal

This POST endpoint is for submitting a signedOrder transaction onchain via the Galaxy Broadcasting System, allowing you to take advantage of gas-free swaps and zaps. This endpoint requires that you submit both the orderId found in the context object and the signature of the signedOrder by the sender. To learn more about signing orders, see the Metamask documentation.

Example

Submitting a signedOrder to zap into Curve.fi am3CRV with USDC on Polygon

Request:

The request must include the orderId and signature of the signedOrder object which was signed by the sender

"orderId": "e63d29b9-e52f-4baf-9b27-6f..."
"signature": "0x5a98f3a678396f0efcdaad1466ab01..."

Response

The response is similar to the GET response, with additional objects including steps and stepsSignedOrder, which describes each step the Portals Router will take to complete the order, a nonce for replay protection, the permitSignature if a gasless approval was used, and a status object which describes the status of the transaction being broadcasted for the sender

{
"tx": {
"data": "0x9d02210100...",
"to": "0xC74063fdb47fe6dCE6d029A489BAb37b167Da57f"
},
"context": {
"orderId": "e63d29b9-e52f-4baf-9b27-6f...",
"isPermit": true,
"minOutputAmount": "4603976404781646314",
"minOutputAmountUsd": 4.969199395294698,
"minOutputAmountSignedOrder": "4563913171814201419",
"minOutputAmountUsdSignedOrder": 4.9259580370573675,
"slippageTolerancePercentage": 0.32,
"gasLimit": "914479",
"inputAmount": "5000555",
"inputAmountUsd": 4.999999938395001,
"inputToken": "polygon:0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"outputToken": "polygon:0xe7a24ef0c5e95ffb0f6684b813a78f2a3ad7d171",
"outputAmount": "4615515192763555202",
"outputAmountUsd": 4.981653529117492,
"outputAmountSignedOrder": "4575493148879352212",
"outputAmountUsdSignedOrder": 4.938456627399818,
"partner": "0x508ee1b661c7DeE089A5b5c3fD234f1058F03c38",
"feeToken": "polygon:0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"feeAmount": "15002",
"feeAmountUsd": 0.015000334778,
"feeAmountSignedOrder": "58204",
"feeAmountUsdSignedOrder": 0.05819723649567414,
"sender": "0x4689CFF824d63117F9C4C42F3EC0001676F00d25",
"recipient": "0x4689CFF824d63117F9C4C42F3EC0001676F00d25",
"target": "0xC74063fdb47fe6dCE6d029A489BAb37b167Da57f",
"value": "0",
"route": ["USDC", "amUSDC", "am3CRV"],
"steps": [
"Transfer 0.015002 USDC to fee collector at polygon:0x508ee1b661c7DeE089A5b5c3fD234f1058F03c38",
"Approve USDC to 0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf",
"Aavev2 deposit from USDC to amUSDC",
"Approve amUSDC to 0x445fe580ef8d70ff569ab36e80c647af338db351",
"Curve add_liquidity from amUSDC to am3CRV",
"Transfer am3CRV to 0x4689CFF824d63117F9C4C42F3EC0001676F00d25"
],
"stepsSignedOrder": [
"Transfer 0.058204 USDC to fee collector at polygon:0x508ee1b661c7DeE089A5b5c3fD234f1058F03c38",
"Approve USDC to 0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf",
"Aavev2 deposit from USDC to amUSDC",
"Approve amUSDC to 0x445fe580ef8d70ff569ab36e80c647af338db351",
"Curve add_liquidity from amUSDC to am3CRV",
"Transfer am3CRV to 0x4689CFF824d63117F9C4C42F3EC0001676F00d25"
],
"routeHash": "0x42bf5bdf545032ce6238759376ad938bc4daf7379a9929d305a500a96373f451"
},
"signedOrder": {
"types": {
"Order": [
{
"name": "inputToken",
"type": "address"
},
{
"name": "inputAmount",
"type": "uint256"
},
{
"name": "outputToken",
"type": "address"
},
{
"name": "minOutputAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
],
"SignedOrder": [
{
"name": "order",
"type": "Order"
},
{
"name": "routeHash",
"type": "bytes32"
},
{
"name": "sender",
"type": "address"
},
{
"name": "deadline",
"type": "uint64"
},
{
"name": "nonce",
"type": "uint64"
}
]
},
"domain": {
"name": "PortalsRouter",
"version": "1",
"chainId": 137,
"verifyingContract": "0xC74063fdb47fe6dCE6d029A489BAb37b167Da57f"
},
"value": {
"order": {
"inputToken": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"outputToken": "0xe7a24ef0c5e95ffb0f6684b813a78f2a3ad7d171",
"inputAmount": "5000555",
"minOutputAmount": "4563913171814201419",
"recipient": "0x4689CFF824d63117F9C4C42F3EC0001676F00d25"
},
"routeHash": "0xfe849489c01c33f9d2ee0f5eabbbc7a1e6c29f7cbcb393464f48637b790a9e79",
"sender": "0x4689CFF824d63117F9C4C42F3EC0001676F00d25",
"deadline": "1691403078",
"nonce": "3"
}
},
"steps": [
...,
{
"inputToken": "0x1a13f4ca1d028320a707d99520abfefca3998b7f",
"target": "0x445fe580ef8d70ff569ab36e80c647af338db351",
"amountIndex": {
"type": "BigNumber",
"hex": "0x01"
},
"data": "0x4515cef30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"needsApproval": true,
"approvalTarget": "0x445fe580ef8d70ff569ab36e80c647af338db351",
"expectedReturn": 0.9999,
"outputToken": "0xe7a24ef0c5e95ffb0f6684b813a78f2a3ad7d171",
"comment": "Curve add_liquidity from amUSDC to am3CRV"
},
...
],
"nonce": "3",
"permitSignature": "0x9fdc1d7...",
"hash": "",
"gelatoTaskId": "0xc148ce99ff485361148f4013540356f7cc2d970f8559175f12de...",
"status": {
"task": {
"chainId": 137,
"taskId": "0xc148ce99ff485361148f4013540356f7cc2d970f8559175f12de...",
"taskState": "WaitingForConfirmation",
"creationDate": "2023-08-07T10:01:23.601Z",
"transactionHash": "0xe5a0878f736f4ecaee5820db8082750c82c390a4216b4620590546e2dcf75e67"
}
}
}

GET v2/portal/status

Example

Getting the status of the signedOrder to zap into Curve.fi am3CRV with USDC on Polygon from the previous example

Request:

As in the POST request, this request must include the orderId and signature of the signedOrder object which was signed by the sender.

"orderId": "e63d29b9-e52f-4baf-9b27-6f..."
"signature": "0x5a98f3a678396f0efcdaad1466ab01..."

Response:

The object returned from this endpoint is identical to the one returned in the POST request described above, allowing you to monitor the status object associated with the transaction. The status object will update throughout the lifecycle of the gasless transaction, from confirmation of receipt through to execution and inclusion in a block

{
"tx": {
...
},
"context": {
...
},
"signedOrder": {
...
},
"steps": [
...
],
"stepsSignedOrder": [
...
],
"nonce": "4",
"permitSignature": "0x9fdc1d7...",
"hash": "",
"gelatoTaskId": "0xbb8c4b6c4...",
"status": {
"task": {
"chainId": 137,
"taskId": "0xbb8c4b6c4...",
"taskState": "WaitingForConfirmation",
"creationDate": "2023-08-07T11:21:43.726Z",
"executionDate": "2023-08-07T11:22:00.006Z",
"transactionHash": "0x12c5a4efbf144b015865acbe62ff8fe0da1d9219d63a63ce3b478e1a267f124a",
"blockNumber": 46016374
}
}
}

Tracking Status

The taskState property in the status object will update to reflect the current state of the transaction. The possible states are:

  • CheckPending: The transaction has been received and is being checked for validity
  • ExecPending: Awaiting execution
  • WaitingForConfirmation: Waiting for block confirmation
  • ExecSuccess: Transaction was successfully executed
  • Cancelled: Transaction was cancelled (i.e., did not enter the ExecPending state) due to an error.
  • ExecReverted: The onchain transaction has reverted.

Once the transactionHash field is populated, you can monitor the transaction using a Web3 library like Ethers.js or directly on Etherscan or equivalent.