Skip to main content
The following open source example app code demonstrates how to implement fiat onramps and cross-chain swaps using the Halliday API. There are also API examples shown to handle withdrawals and retries for payments that have failed to complete.

Fiat onramps via API using JavaScript

This example app shows how to build a fiat-to-crypto onramp, with a custom user interface, using the Halliday Payments API.
In the example, a user would be able to onramp directly to IP on Story mainnet with a provider like Stripe, Transak, or Moonpay. The app allows a user to input their Story mainnet address and the amount of USD they wish to spend. Using JavaScript, the app fetches a collection of quotes from the API and displays the best price available with each provider, as well as the total amount of onramp fees.
// From getQuote function

const res = await fetch('https://v2.prod.halliday.xyz/payments/quotes', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    request: {
      kind: 'FIXED_INPUT',
      fixed_input_amount: {
        asset: inputAsset,
        amount: inputAmount,
      },
      output_asset: outputAsset,
    },
    price_currency: 'usd',
    onramps,
    onramp_methods: fiatOnrampPayInMethods,
    customer_geolocation: { alpha3_country_code: 'USA' }
  }),
});
Once the user selects a quote and destination address in the user interface, clicking or tapping the Continue button confirms the quote with the API.
// From acceptQuote function

const res = await fetch('https://v2.prod.halliday.xyz/payments/confirm', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    payment_id: selectedQuote.paymentId,
    state_token: selectedQuote.stateToken,
    owner_address: destinationAddress,
    destination_address: destinationAddress
  })
});
Next, the payment provider’s checkout page is shown to the user. This is where the user would input their credit or debit card information for the transaction.
// From onContinueButtonClick function

const acceptedQuote = await acceptQuote();
const paymentId = acceptedQuote.payment_id;
const onrampUrl = acceptedQuote.next_instruction.funding_page_url;

paymentStatusInterval = setInterval(async () => {
  console.log('payment status:', paymentId, await getPaymentStatus(paymentId));
}, 5000);

continueButton.classList.remove('loading');

onrampIframe.src = onrampUrl;
Once the checkout is completed, the payment workflow begins. The app polls the payment status endpoint and shows the current status of the payment in the user interface.
// From getPaymentStatus function

const res = await fetch(`https://v2.prod.halliday.xyz/payments?payment_id=${paymentId}`, {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
    'Content-Type': 'application/json'
  },
});
This example does not show an implementation of a recovery flow if the payment fails midway onchain. In the event a payment does not complete, a user can recover it by connecting the owner wallet on this page: https://app.halliday.xyz/funding/${payment_id}.

Cross-chain swaps via API using JavaScript

This example app shows how to build a cross-chain swap app, with a custom user interface, using the Halliday Payments API.
In the example, a user would be able to swap from USDC on Base to IP on Story mainnet using the Halliday Payments API and their own wallet. The app allows a user to connect their wallet and input the amount of USDC on Base that they wish to spend. Using JavaScript, the app fetches a collection of quotes from the API and displays the best price available and the total amount of fees.
// From getQuote function

const res = await fetch('https://v2.prod.halliday.xyz/payments/quotes', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    request: {
      kind: 'FIXED_INPUT',
      fixed_input_amount: {
        asset: inputAsset,
        amount: inputAmount
      },
      output_asset: outputAsset
    },
    price_currency: 'USD'
  })
});
Once the user approves the quote, it is confirmed using the API.
const requestBody = {
  payment_id: quote.paymentId,
  state_token: quote.stateToken,
  owner_address: userAddress,
  destination_address: userAddress
};

const res = await fetch('https://v2.prod.halliday.xyz/payments/confirm', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(requestBody)
});
Next, the user can sign transactions with their wallet to fund the workflow.
The latest status of the cross-chain swap is fetched from the API and displayed in the UI.
const res = await fetch('https://v2.prod.halliday.xyz/payments' +
  `?payment_id=${paymentId}`, {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
    'Content-Type': 'application/json'
  }
});
The next section shows how to implement payment recoveries directly via API. Alternatively, payments can be recovered on the following page by connecting the payment’s owner address wallet https://app.halliday.xyz/funding/${payment_id}.

Withdraw stuck assets with the API using JavaScript

In the event that a payment begins its onchain steps and fails to complete them, or assets are sent to a processing address in which the payment is expired, a user can sign a transaction to withdraw the tokens to any address. The owner address, which is usually the user’s wallet, is the sole controller of assets in the processing addresses. An overview and details on the withdrawal process are explained on the API Recoveries & Errors page.
The app uses the history API endpoint to fetch payments for the wallet address.
async function getWalletPaymentHistory(address, paginationKey) {
  const params = new URLSearchParams({
    category: 'ALL',
    owner_address: address,
    ...(paginationKey && { pagination_key: paginationKey })
  });

  const res = await fetch(`https://v2.prod.halliday.xyz/payments/history?${params}`, {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
      'Content-Type': 'application/json'
    }
  });

  // Note that only payments initialized with this HALLIDAY_API_KEY will be returned
  const data = await res.json();
  return data;
}
Here is example code to get the entire history of payments created using the API.
const payments = [];
let paginationKey;
do {
  const history = await getWalletPaymentHistory(_userAddress, paginationKey);
  if (history.next_pagination_key) {
    paginationKey = history.next_pagination_key;
  } else {
    paginationKey = undefined;
  }
  payments.push(...history.payment_statuses);
} while (paginationKey)
Next, after filtering out properly completed payments, the balance of failed or expired payments is queried using the API one by one. This endpoint will get the current token balances for all of the payment’s processing addresses on the relevant chains and return the data in the response.
  async function getProcessingAddressBalances(paymentId) {
    try {
      const res = await fetch(`https://v2.prod.halliday.xyz/payments/balances`, {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer ' + HALLIDAY_API_KEY,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ payment_id: paymentId })
      });

      const data = await res.json();
      return data;
    } catch (e) {
      console.error('getProcessingAddressBalances error', e);
    }
  }
Relevant payment information is rendered in the UI for the user. The user can tap or click buttons to initialize the withdrawal flow for each individual stuck token balance. Initializing the withdrawal flow has three steps:
  1. Get the EIP-712 data from the API (getTypedData) which is used to sign a transaction to withdraw the token from the processing address.
  2. The transaction is signed (signTypedData) with the user wallet using Ether.js on the client.
  3. The signed transaction data is then sent to the API (confirmWithdrawal) to confirm the withdrawal and execute it onchain. The API returns the onchain transaction hash in the response body.
withdrawButton.addEventListener('click', async () => {
  withdrawButton.classList.add('loading');

  // Fetch the withdraw signature data from the API
  const withdrawToAddress = userAddress; // user's connected wallet
  const typedDataToSign = await getTypedData(withdrawToAddress, paymentId, balance.token, balance.value.amount);
  const { domain, types, message } = JSON.parse(typedDataToSign.withdraw_authorization);
  delete types.EIP712Domain;

  // Sign the withdraw transaction using Ethers
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const signature = await signer.signTypedData(domain, types, message);

  // Send signature to API to be posted onchain
  const txHash = await confirmWithdrawal(withdrawToAddress, paymentId, balance.token, balance.value.amount, signature);

  // Show the resulting withdraw transaction on the proper block explorer
  const chain = balance.token.split(':')[0];
  const { explorer } = supportedChains[chain];
  const link = item.querySelector('a');
  link.setAttribute('href', `${explorer}tx/${txHash}`);
  link.classList.remove('hidden');

  withdrawButton.disabled = true;
  withdrawButton.classList.remove('loading');
});
Lastly, the transaction with the final transfer of tokens to the withdrawal address is rendered as a blockchain explorer link for the user to tap or click.

Retry incomplete payments with the API using JavaScript

In the event an onramp or swap is funded but fails to complete, a retry can be attempted using assets lingering in a processing address. A retry requires a new quote which uses the lingering assets as the input token amount. Think of retries as a withdrawal of an old payment to deposit into a new payment. The owner address is the sole controller of assets in the processing addresses. The user wallet must sign a transaction to initialize the retry. Payment retries are further explained on the API Recoveries & Errors page.
The retry example uses the same history API endpoint to fetch payments for the wallet address as the above withdraw example (see getWalletPaymentHistory above). Also the same endpoint to fetch current processing address balances is used (see getProcessingAddressBalances above). Using the balances response, a quote is made for bridging and swapping with the processing address balance as the input. This is the same quotes endpoint used in onramps and swaps. See the getQuote function in the cross-chain swaps example above.
const balance = balances.balance_results[i];
const _token = balance.token;
const _amount = +balance.value.amount;
if (_amount === 0) {
  continue;
}

const quoteResult = await getQuote(balance.value.amount, _token, outputAsset, paymentId);

if (quoteResult.quotes.length === 0) {
  alert('Retry not possible. Try withdrawal.');
  continue;
}
Once the user accepts a quote available to retry the payment, the following four steps orchestrate a retry:
  1. Use the API endpoint to accept the new quote (see acceptQuote). This is the same endpoint used to accept quotes in onramps and swaps.
  2. Fetch the withdrawal signature data from the API (see getTypedData). This transaction will transfer tokens from the old payment to the new payment once it is executed onchain. This is the same gas-sponsored transaction concept explained in the above withdraw example.
  3. The transaction is signed (signTypedData) with the user wallet using Ether.js on the client.
  4. The signed transaction data is then sent to the API (confirmWithdrawal) to confirm the token transfer from the old payment to the new payment. The API then executes this transaction onchain covering all of the blockchain gas costs.
// Accept the retry quote
const acceptQuoteRequest = await acceptQuote(newPaymentId, newStateToken);
const statusElement = document.getElementById('status');
statusElement.innerText = `Status: ${acceptQuoteRequest.status}...`;

// Fetch the retry signature data from the API
const withdrawToAddress = acceptQuoteRequest.next_instruction.deposit_info[0].deposit_address; // new quoted payment's deposit address
const typedDataToSign = await getTypedData(withdrawToAddress, paymentId, balance.token, balance.value.amount);
const { domain, types, message } = JSON.parse(typedDataToSign.withdraw_authorization);
delete types.EIP712Domain;

// Sign the retry transfer transaction using Ethers
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const signature = await signer.signTypedData(domain, types, message);

// Send signature to API to be posted onchain
await confirmWithdrawal(withdrawToAddress, paymentId, balance.token, balance.value.amount, signature);
The new payment ID can subsequently be tracked. The new payment will reach the COMPLETED state once the output tokens have reached the destination address.