Skip to content
141 changes: 69 additions & 72 deletions docs/how-arbitrum-works/timeboost/how-to-use-timeboost.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@ author: jose-franco
content_type: how-to
---

Timeboost is a new transaction ordering policy for Arbitrum chains. With Timeboost, anyone can bid for the right to access an express lane on the **Sequencer** for faster transaction inclusion.
Timeboost is a transaction ordering policy for Arbitrum chains. With Timeboost, anyone can bid for the right to access an express lane on the **Sequencer** for faster transaction inclusion.

In this how-to, you'll learn how to bid for the right to use the express lane and submit transactions through the express lane. To learn more about Timeboost and the key terms used on this page, refer to the [gentle introduction](/how-arbitrum-works/timeboost/gentle-introduction.mdx).

This how-to assumes that you're familiar with the following:

- [How Timeboost works](/how-arbitrum-works/timeboost/gentle-introduction.mdx)
- [viem](https://viem.sh/), since the snippets of code present in the how-to use this library
This how-to assumes that you're familiar with [How Timeboost works](/how-arbitrum-works/timeboost/gentle-introduction.mdx)

<VanillaAdmonition type="note" title="Note about transferring express lane rights">

Please note that in the initial release of Timeboost, transferring control of the express lane via either the `setTransferor` or the `transferExpressLaneController` will not be supported by the Arbitrum Nitro node software at launch and may be implemented at a future date via a regular node upgrade. Calls made to these two functions on the auction contract will be successful, but the node software (including the Sequencer) will not recognize the actual rights transfer.

A round's express lane controller, at their choice, can still send transactions signed by others on a per-transaction basis, as explained later in this guide.
A round's express lane controller, at their choice, can send transactions signed by others on a per-transaction basis, as explained later in this guide.

</VanillaAdmonition>

Expand All @@ -45,7 +40,7 @@ Before we begin, make sure you have:

The following table shows this information for the Arbitrum DAO-owned chains:

#### Table 1: Timeboost reference URLs and addresses
#### Timeboost reference URLs and addresses

| Network | Auction contract | Autonomous auctioneer endpoint |
| ---------------- | ----------------------------------------------------------------------------- | ------------------------------------------ |
Expand All @@ -60,6 +55,8 @@ Before bidding on an auction, we need to deposit funds in the auction contract.
To see the amount of tokens we have deposited in the auction contract, we can call the function `balanceOf` in the `Auction` contract:

```tsx
// Code example uses viem

const depositedBalance = await publicClient.readContract({
address: auctionContractAddress,
abi: auctionContractAbi,
Expand All @@ -72,6 +69,8 @@ console.log(`Current balance of ${userAddress} in auction contract: ${depositedB
If we want to deposit more funds to the `Auction` contract, we first need to know what the bidding token is. To obtain the address of the bidding token, we can call the function `biddingToken` in the `Auction` contract:

```tsx
// Code example uses viem

const biddingTokenContractAddress = await publicClient.readContract({
address: auctionContractAddress,
abi: auctionContractAbi,
Expand All @@ -89,6 +88,8 @@ On Arbitrum One and Arbitrum Nova, the default bidding token is **WETH**.
Once we know what the bidding token is, we can deposit funds to the auction contract by calling the function `deposit` of the contract after having it approved as spender of the amount we want to deposit:

```tsx
// Code example uses viem

// Approving spending tokens
const approveHash = await walletClient.writeContract({
account,
Expand All @@ -110,13 +111,45 @@ const depositHash = await walletClient.writeContract({
console.log(`Deposit transaction sent: ${depositHash}`);
```

### Step 2: Submit bids
### Step 2: query the reserve pricer API

Any auction participant submitting a bid must first know the minimum acceptable bid amount for the round being bid on. The Timeboost Reserve Pricer API exposes this data publicly over HTTP, returning the current reserve price and the round it applies to. Query this API before submitting any bid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any auction participant submitting a valid bid must first check the minimum acceptable bid amount for the round being bid on. The reserve price for a round is available via API 5 seconds before the auction closes.


The reserve price is updated at the 31st second of every minute.

#### Endpoints

Comment thread
anegg0 marked this conversation as resolved.
The API exposes two endpoints over HTTPS:

- `/api/latest` returns the newest reserve price and round.
- `/api/recent` returns the reserve price and rounds for the last two hours.

The base URL differs per network:

| Network | Base URL |
| ---------------- | ----------------------------------------------- |
| Arbitrum Sepolia | `https://arbsepolia-reserve-pricer.arbitrum.io` |
| Arbitrum One | `https://arb1-reserve-pricer.arbitrum.io` |

For example, to fetch the latest reserve price and round:

```shell
# Arbitrum One
curl https://arb1-reserve-pricer.arbitrum.io/api/latest

# Arbitrum Sepolia
curl https://arbsepolia-reserve-pricer.arbitrum.io/api/latest
```

### Step 3: submit bids

Once we have deposited funds into the auction contract, we can submit bids for the current auction round.

We can obtain the current round by calling the function `currentRound` in the `Auction` contract:

```tsx
// Code example uses viem

const currentRound = await publicClient.readContract({
address: auctionContractAddress,
abi: auctionContractAbi,
Expand All @@ -128,6 +161,8 @@ console.log(`Current round: ${currentRound}`);
The above shows the current round that's running. At the same time, the auction for the next round might be open. For example, if the `currentRound` is 10, the auction for round 11 is currently happening. To check whether or not that auction is open, we can call the function `isAuctionRoundClosed` of the `Auction` contract:

```tsx
// Code example uses viem

let currentAuctionRoundIsClosed = await publicClient.readContract({
address: auctionContractAddress,
abi: auctionContractAbi,
Expand All @@ -143,6 +178,12 @@ Remember that, by default, auctions for a given round open 60 seconds before tha

Once we know the current round, we can bid for (`currentRound + 1`) and verify that the auction is still open (`!currentAuctionRoundIsClosed`), then we can submit a bid.

:::tip Fetching the minimum bid amount

Before submitting a bid, query the reserve pricer API to retrieve the minimum acceptable bid amount for the round you're bidding on.

:::

When bids get submitted to the autonomous auctioneer endpoint, we need to send an `auctioneer_submitBid` request with the following information:

- chain id
Expand All @@ -152,15 +193,11 @@ When bids get submitted to the autonomous auctioneer endpoint, we need to send a
- the amount in `wei` of the deposit **ERC-20** token to bid
- signature (explained below)

<VanillaAdmonition type="info" title="Minimum reserve price">

The amount to bid must be above the minimum reserve price at the moment you are bidding. This parameter is configurable per chain. You can obtain the minimum reserve price by calling the method `minReservePrice()(uint256)` in the `Auction` contract.

</VanillaAdmonition>

Let's see an example of a call to this RPC method:

```tsx
// Code example uses viem

const currentAuctionRound = currentRound + 1;
const hexChainId: `0x${string}` = `0x${Number(publicClient.chain.id).toString(16)}`;

Expand Down Expand Up @@ -195,6 +232,8 @@ The signature that needs to be sent is an [EIP-712](https://eips.ethereum.org/EI
Here's an example to produce that signature with viem:

```tsx
// Code example uses viem

const currentAuctionRound = currentRound + 1;

const signatureData = hashTypedData({
Expand Down Expand Up @@ -231,7 +270,7 @@ You can also call the function `getBidHash` in the auction contract to obtain th

When sending the request, the autonomous auctioneer will return an empty result with an HTTP status `200` if received correctly. If the result returned contains an error message, something went wrong. Following are some of the error messages that can help us understand what's happening:

#### Table 2: Errors relating to bid submission
#### Errors relating to bid submission

| Error | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------------------- |
Expand All @@ -243,7 +282,7 @@ When sending the request, the autonomous auctioneer will return an empty result
| `RESERVE_PRICE_NOT_MET` | Bid amount does not meet the minimum required reserve price onchain |
| `INSUFFICIENT_BALANCE` | The bid amount specified in the request is higher than the deposit balance of the depositor in the contract |

### Step 3: find out the winner of the auction
### Step 4: find out the winner of the auction

After the auction closes and before the round starts, the autonomous auctioneer will call the auction contract with the two highest bids received, allowing the contract to declare the winner and deduct the second-highest bid from the winner's deposited funds. After this, the contract will emit an event with the new Express Lane Controller address.

Expand All @@ -263,6 +302,8 @@ event SetExpressLaneController(
Here's an example to get the log from the auction contract to determine the new express lane controller:

```tsx
// Code example uses viem

const fromBlock = <any recent block, for example during the auction>
const logs = await publicClient.getLogs({
address: auctionContractAddress,
Expand Down Expand Up @@ -305,6 +346,8 @@ Timeboost doesn't currently support the `eth_sendRawTransactionConditional` meth
Let's see an example of a call to this RPC method:

```tsx
// Code example uses viem

const hexChainId: `0x${string}` = `0x${Number(publicClient.chain.id).toString(16)}`;

const transaction = await walletClient.prepareTransactionRequest(...);
Expand Down Expand Up @@ -344,6 +387,8 @@ The required signature is an Ethereum signature that needs to be sent with the f
Here's an example to produce that signature:

```tsx
// Code example uses viem

const hexChainId: `0x${string}` = `0x${Number(publicClient.chain.id).toString(16)}`;

const transaction = await walletClient.prepareTransactionRequest(...);
Expand All @@ -364,7 +409,7 @@ const signature = await account.signMessage({

When sending the request, the sequencer will return an empty result with an HTTP status `200` if it received it correctly. If the result returned contains an error message, something went wrong. Following are some of the error messages that can help us understand what's happening:

#### Table 3: Errors relating to express lane transaction submission
#### Errors relating to express lane transaction submission

Note that if you get any of the errors below, then the sequence number used in your express lane transaction was _not_ consumed.
| Error | Description |
Expand All @@ -385,65 +430,15 @@ If you are not the express lane controller and you try to submit a transaction t

:::

<!--

## How to transfer the right to use the express lane to someone else

If you are the express lane controller, you also have the right to transfer the right to use the express lane to someone else.

To do that, you can call the function `transferExpressLaneController` in the auction contract:

```tsx
const transferELCTransaction = await walletClient.writeContract({
currentELCAccount,
address: auctionContractAddress,
abi: auctionContractAbi,
functionName: 'transferExpressLaneController',
args: [currentRound, newELCAddress],
});
console.log(`Transfer EL controller transaction hash: ${transferELCTransaction}`);
```

From that moment, the previous express lane controller will not be able to send new transactions to the express lane.

### Setting a transferor account

A `transferor` is an address with the right to transfer express lane controller rights on behalf of the express lane controller. This function (`setTransferor`) ensures that the express lane controller has a way of nominating an address that can transfer rights to anyone they see fit to improve the user experience of reselling/transferring the control of the express lane.

We can set a transferor for our account using the auction contract. Additionally, we can fix that transferor account until a specific round to guarantee other parties that we will not change the transferor until the specified round finishes.

To set a transferor, we can call the function `setTransferor` in the auction contract:

```tsx
// Fixing the transferor for 10 rounds
const fixedUntilRound = currentRound + 10n;

const setTransferorTransaction = await walletClient.writeContract({
currentELCAccount,
address: auctionContractAddress,
abi: auctionContractAbi,
functionName: 'setTransferor',
args: [
{
addr: transferorAddress,
fixedUntilRound: fixedUntilRound,
},
],
});
console.log(`Set transferor transaction hash: ${setTransferorTransaction}`);
```

From that moment on (until the transferor is changed or disabled), the transferor will be able to call `transferExpressLaneController` while the express lane controller is `currentELCAccount` to transfer the rights to use the express lane to a different account.

-->

Comment thread
anegg0 marked this conversation as resolved.
## How to withdraw funds deposited in the auction contract

Funds are deposited in the auction contract to have the right to bid in auctions. Withdrawing funds is possible through a two-step process: initiate the withdrawal, wait for two rounds, and then finalize the withdrawal.

To initiate a withdrawal, we can call the function `initiateWithdrawal` in the `Auction` contract:

```tsx
// Code example uses viem

const initWithdrawalTransaction = await walletClient.writeContract({
account,
address: auctionContractAddress,
Expand All @@ -468,6 +463,8 @@ In this event, the `account` is the address from which we will withdraw funds, `
After two rounds have passed, we can call the method `finalizeWithdrawal` in the `Auction` contract to finalize the withdrawal:

```tsx
// Code example uses viem

const finalizeWithdrawalTransaction = await walletClient.writeContract({
account,
address: auctionContractAddress,
Expand Down Expand Up @@ -512,7 +509,7 @@ In the sequencer feed, the `BroadcastFeedMessage` struct now contains a `blockMe

In the current implementation, information about the winning bid for a resolved auction emits via the `AuctionResolved` event ([sample interface](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/express-lane-auction/IExpressLaneAuction.sol#L95-L103)). Historical bid information, including the round number and bid amounts, are published to a public S3 bucket at a regular cadence. The domain for the S3 bucket where historical bids get saved is:

#### Table 4: S3 URLs for historical bid data
#### S3 URLs for historical bid data

:::info URL updates for historical bid data
On June 9 2025, at around 17:27 ET (UTC−05:00), the region in which the Amazon S3 bucket for historical bid data on Arbitrum One was _changed_ from `s3://timeboost-auctioneer-arb1/uw2/validated-timeboost-bids/` to `s3://timeboost-auctioneer-arb1/ue2/validated-timeboost-bids/`. The below table has been updated for the new, correct URL to access bid data after June 9, 2025 17:27 ET, but if you require data from before June 9, 2025 17:27 ET, then please use `s3://timeboost-auctioneer-arb1/uw2/validated-timeboost-bids/`.
Expand Down
Loading