owned this note
owned this note
Published
Linked with GitHub
# Metaplex Auction House
## Introduction
Auction house is a transaction protocol for marketplaces to implement a decentralised, escrow-less, stateless sales contract.
Key features of the Auction House contract are the following:
* No more escrowing NFTs - the NFTs remain in your wallet till the time the sale goes through
* No more making an offer on an NFT and having to return to the site to execute the final sale - it just happens
* Secondaries can now do price matching on your behalf - easier to find potential matches for your NFTs and hence makes NFTs more liquid than before
* Secondaries can pay your fees for you - sell NFTs with 0 SOL in your wallet
## Escrow-less
The community aspect, of auctioning NFTs without the NFTs actually leaving the wallets till the time the sale goes through, is immensely important:
1. This enables you to auction your NFT in multiple (escrow-less) marketplaces that are powered by the Auction House contract
2. Proof of Ownership: don’t miss out on free airdrops and staking rewards or give up access to permissioned discord servers
3. Safety: you don’t have to rely on the marketplaces’ escrow contracts to safely hold your NFTs.
## How to follow this guide?
This guide aims at explaining all the concepts regarding the Auction House protocol and how it works, while also giving the readers easy-to-read code-snippets to jump-start their own Auction House powered marketplaces / DEXs.
We would recommend following each section in detail to understand the inner mechanisms. This will make it easier to understand the code snippets as well, as they use the Accounts, PDAs and Instructions explained in the guide.
There are some code snippets which are repetitive. To avoid having a very lengthy guide, we will show these snippets once and mention these snippets with a mnemonic wherever they are to be re-used.
## Installation
Install the required packages to start interacting with Auction House
```
yarn add @metaplex-foundation/js @solana/web3.js
npm i @metaplex-foundation/js @solana/web3.js
```
## Setup
After installing the required package lets import the Metaplex Class and some functions from the solana web3 library inorder to interact with the Solana Blockchain.
```tsx
import { Metaplex } from "@metaplex-foundation/js";
import { Connection, clusterApiUrl } from "@solana/web3.js";
const connection = new Connection('https://api.devnet.solana.com');
const metaplex = new Metaplex(connection);
```
A wallet or storage can also be passed to the metaplex instance by using a slightly modified version of the API.
```tsx
const metaplex = Metaplex.make(connection)
.use(keypairIdentity(wallet))
.use(bundlrStorage());
```
If everything goes well, we can start looking more into how to interact with the Auction house module of the JS SDK.
# How to create an Auction House ?
The JS SDK provides us with a function called `createAuctionHouse` which is used to create an Auction House instance you can provide all the data that are defined in the `AuctionHouse` Struct to create one. Lets look into how we can do that.
The JS SDK allows us to create an auction house with minimum configuration. With this it sets the authority of the auction house instance to the signer of the transaction and by default it sets the treasury mint to SOL. This is done in case of minimum configuration.
```json
const {auctionHouse} = await mx
.auctions()
.createAuctionHouse({
sellerFeeBasisPoints: 200,
})
.run();
```
To configure the Auction House without minimum configuration you can pass all the values yourself in this way:
```tsx
// USDC Mint Address in Devnet
const treasuryMint = new PublicKey(
'3dLdxAs35CXmgi2DfQvgHQdS742JRsvNqSVi8MYsVrv8'
);
// The Auction House Authority Address
const authority = mx.identity();
// The Address from which the fees are going to be paid when a sale is executed
const feeWithdrawalDestination = new PublicKey(
'DqBqTZsffE22kNMpZactpuiqHMp9bLGzcuxhRTfSbnsk'
);
// The Address in which the Auction House fees gets collected
const treasuryWithdrawalDestinationOwner = new PublicKey(
'7EgtopgaQngfAqLmrRzJjBHVcfWZE8vm8tZ3rKTCdYZR'
);
const {auctionHouse} = await mx
.auctions()
.createAuctionHouse({
sellerFeeBasisPoints: 200,
requiresSignOff: true,
canChangeSalePrice: true,
treasuryMint: treasuryMint,
payer: authority,
authority: authority.publicKey,
feeWithdrawalDestination: feeWithdrawalDestination,
treasuryWithdrawalDestinationOwner: treasuryWithdrawalDestinationOwner,
})
.run();
```
## How does Auction House work?
We'll first dive deep into how the Auction House protocol works, and then we'll proceed into the additional features which supercharges this protocol.
To dive deep into the mechanisms, lets first see all the accounts that are involved / created during the auction process.
## Accounts

The Auction House protocol comprises of 5 main Accounts:
1. Auction House Instance
2. Seller Trade State
3. Buyer Trade State
4. Auction House Buyer Escrow
5. Auction House Treasury Account
There are other accounts as well but they play a supporting role to the above accounts.
### Auction House Instance

* This is a program derived address (PDA)
* These instances are in the keyspace of the AH program, ie, are public keys owned by the AH program
* This Instance is owned by the Authority, which can update the AH instance and create Access Control
All this data can be fetched using the JS SDK. Lets look into how we can do that:
```tsx
// This is a dummy auction house on Devnet use this or any other auction house address
const auction_house_address = "Gr31akcY9wb7Gsnu4ej39MydBfxcT8mehZtKxFTsCAai"
(async() => {
const ahData = await mx
.auctions()
.findAuctionHouseByAddress(auction_house_address)
.run();
console.log(JSON.stringfy(ahData))
})()
```
While Logging it out you are going to get all the data defined in the above struct i.e:
```json
{
"model": "auctionHouse",
"address": "Gr31akcY9wb7Gsnu4ej39MydBfxcT8mehZtKxFTsCAai",
"creatorAddress": "Eqp8mRhQpjk2nDYtZJjkyshJBc5EZudPDcMNhaXGuyJN",
"authorityAddress": "Eqp8mRhQpjk2nDYtZJjkyshJBc5EZudPDcMNhaXGuyJN",
"treasuryMint": {
"model": "mint",
"address": "So11111111111111111111111111111111111111112",
"mintAuthorityAddress": null,
"freezeAuthorityAddress": null,
"decimals": 9,
"supply": {
"basisPoints": "00",
"currency": {
"symbol": "SOL",
"decimals": 9,
"namespace": "spl-token"
}
},
"isWrappedSol": true,
"currency": {
"symbol": "SOL",
"decimals": 9,
"namespace": "spl-token"
}
},
"feeAccountAddress": "AnUxX7EmyRZegQtv4xYaBBWMaYG5uoLdJdMHfWXu4CLo",
"treasuryAccountAddress": "FxaGuPmq9qNDX1qWAMrGUfwGp3cqahW1T5eViPDG7VR2",
"feeWithdrawalDestinationAddress": "Eqp8mRhQpjk2nDYtZJjkyshJBc5EZudPDcMNhaXGuyJN",
"treasuryWithdrawalDestinationAddress": "Eqp8mRhQpjk2nDYtZJjkyshJBc5EZudPDcMNhaXGuyJN",
"sellerFeeBasisPoints": 500,
"requiresSignOff": false,
"canChangeSalePrice": false,
"isNative": true
}
```
#### Important parameters of Instance
* **Treasury Withdraw Destination**: The wallet that receives the AuctionHouse fees.
* **Fee Withdraw Destination**: In AH, the marketplace needs to pay the fees for selling and buying, and not the user. This points to the account from which the fees is withdrawn in case of a sale goes through
* **Seller Fee Basis Points**: The share of the sale the auction house takes on all NFTs.
* **Treasury Mint**: The SPL token you accept as the purchase currency
#### requireSignOff
This allows the centralised MPs to gate which NFT can be listed, bought and sold! For every action: list, bid, sale, the instance authority needs to sign the tx.
This also allows the MPs to implement their own order matching algorithms (see next slide). Decentralised MPs should keep requireSignOff as False, and they can use other means like Allow Lists using Merkele Trees and UI gating to gate which NFTs get listed.
This allows for an easy way for MPs to index data required to keep track of all offers/bids/sales/change in token accounts etc (more info on why this is required later).
#### canChangeSalePrice
`canChangeSalePrice` is only intended to be used with AuctionHouses that Requires Sign Off. If the buyer intentionally lists their NFT for a price of 0, a new `FreeSellerTradeState` is made.
The Auction House can then change the sale price to match a matching Bid that is greater than 0. This allows the Auction house to do complicated order matching to find the best price for the seller.
The Auction House can only sell it for 0 if you sign the transaction with your key, but currently it can sell it for an arbitarily low price, e.g. 1 lamport.
### Trade States

Trade States are PDAs that represent, what are effectively a bid or an offer for an NFT.
There are two main trade states: Seller Trade State and Buyer Trade State for an offer and a bid respectively.
There is also a third trade state: Free Seller Trade State.
* remember canChangeSalePrice and user selling an NFT for 0 SOL?
* when the user auctions an NFT for 0 SOL, AH builds the Free Seller Trade State using which it creates new sales at different prices.
Trade states only have 1 byte of data, and only store 1 thing: bump (because its a PDA, it has to store a bump)
It's a pattern to show that this account is real: since it's a PDA and the address is owned by AH contract, no one can make fake bids and listings. They are a hash of the ordered data: type of NFT, mint address, token amount which can be x for a fungible asset/tokens, offer price. All this information, in that order, is hashed into the seeds of the PDA
This is primarily done to cut down the costs: trade states are very cheap.
There is one major issue here: How do you figure out the list of bids / offers / sales / changes in token accounts if nothing is stored in the Trade States? We'll talk about this in later sections.
### Program As Signer Account

`programAsSigner` account is the program derived address which the Auction House assigns as the Delegate to `executeSale` once an Offer and a Bid (trade states) match.
We will talk about Delegates and `executeSale` in much greater detail in the coming sections.
### Buyer Escrow Account

When a trade is executed, the amount of the sale is debited from the buyer's escrow account. This has a slight manual step associated to it: this account needs to be topped off (manually) if the user is buying multiple NFTs.
There are Deposit and Withdraw instructions to deposit and withdraw tokens (native or SPL) from buyer escrow accounts.
## Auction in action
Now that we know about all the (major) accounts that are involved in a Auction, lets dive deep into how the Auction actually works.
The Auction is divided into three main Instructions:
1. Sell
2. Buy
3. ExecuteSale
There are other supporting instructions as well like withdrawFee, withdrawTreasury that I'll talk about later.
## Sell Order
The seller posts an offer to "sell" an NFT. When this happens, the Auction House creates the Seller trade state PDA. Auction House then assigns the programAsSigner PDA as the "Delegate"
* Delegates are a feature of the Solana SPL-token program: https://spl.solana.com/token#authority-delegation.
* Delegates are assigned by the authority using the "Approve" instruction
This allows a key signer or authority to take a quantity of tokens out of a token account while the delegate is valid.
A token account can only have 1 delegate. As an NFT comprises of 1 token, Auction House pulls the entire asset from the seller when the sale goes through (at a later point)
Here is how you can make the create a Sell Order using JS:
```tsx
export const createSell = async (connection: anchor.web3.Connection) => {
try {
// Accounts/Publickeys
const mint = new anchor.web3.PublicKey('');
const auctionHousePubKey = new anchor.web3.PublicKey('');
const feeAccountPubkey = new anchor.web3.PublicKey('');
const metadataPubkey = new anchor.web3.PublicKey('');
const freeSellerTradeState = new anchor.web3.PublicKey('');
const tokenAccountPubkey = (await getAtaForMint(mint, wallet.publicKey))[0];
const [programAsSigner, programAsSignerBump] =
await getAuctionHouseProgramAsSigner();
const accounts: SellInstructionAccounts = {
auctionHouse: auctionHousePubKey,
auctionHouseFeeAccount: feeAccountPubkey,
authority: wallet.publicKey,
metadata: metadataPubkey,
wallet: wallet.publicKey,
freeSellerTradeState: freeSellerTradeState,
programAsSigner: programAsSigner,
tokenAccount: tokenAccountPubkey,
sellerTradeState: programAsSigner
};
// const args: SellInstructionArgs = {};
} catch (error) {
console.log('SELL ERROR', error);
}
};
```
## Buy Order
Similar to the sell order, when a buyer places a bid, Auction House creates the buyer trade state PDA representing the bid. The buy order instruction then transfers tokens (native, or SPL tokens) to the aforementioned Buyer Escrow Account.
In case of an SPL token, an associated token account gets created first at the buyer escrow account's location before transferring the SPL tokens.
The beauty of separate buy and sell orders is that one can post a bid for a non-listed NFT one prefers. This allows for the non-sellers to allure attractive bids for their NFTs, and vice-versa allows buyers to attract otherwise non-interested sellers. I call that a win-win.
This is how you can make a Buy Order using JS:
```tsx
export const buy = async (connection: anchor.web3.Connection) => {
try {
const mint = new anchor.web3.PublicKey('');
const tokenAccount = new anchor.web3.PublicKey('');
const NFTMintData = await getMint(connection, mint);
const tokenSizeAdjusted = new anchor.BN(
await getPriceWithMantissa(1, NFTMintData.decimals)
);
const TreasuryMintData = await getMint(
connection,
new anchor.web3.PublicKey(AuctionHouse.mint)
);
const buyPriceAdjusted = new anchor.BN(
await getPriceWithMantissa(1, TreasuryMintData.decimals)
);
const [escrowPaymentAccount, escrowBump] = await getAuctionHouseBuyerEscrow(
new anchor.web3.PublicKey(AuctionHouse.address),
wallet.publicKey
);
const [BuyertradeState, BuyertradeBump] = await getAuctionHouseTradeState(
new anchor.web3.PublicKey(AuctionHouse.address),
wallet.publicKey,
tokenAccount,
new anchor.web3.PublicKey(AuctionHouse.mint),
mint,
tokenSizeAdjusted,
buyPriceAdjusted
);
const accounts: BuyInstructionAccounts = {
auctionHouse: new anchor.web3.PublicKey(AuctionHouse.address),
auctionHouseFeeAccount: new anchor.web3.PublicKey(
AuctionHouse.feeAccount
),
authority: new anchor.web3.PublicKey(AuctionHouse.authority),
metadata: await getMetadata(mint),
wallet: wallet.publicKey,
// Token Account of the Token to purchase
tokenAccount: tokenAccount,
treasuryMint: new anchor.web3.PublicKey(AuctionHouse.mint),
buyerTradeState: BuyertradeState,
escrowPaymentAccount: escrowPaymentAccount,
paymentAccount: wallet.publicKey,
transferAuthority: wallet.publicKey
};
const args: BuyInstructionArgs = {
buyerPrice: buyPriceAdjusted,
escrowPaymentBump: escrowBump,
tokenSize: tokenSizeAdjusted,
tradeStateBump: BuyertradeBump
};
const instruction = createBuyInstruction(accounts, args);
const { blockhash } = await connection.getLatestBlockhash();
const transaction = new anchor.web3.Transaction({
recentBlockhash: blockhash
});
transaction.add(instruction);
wallet.signTransaction(transaction);
const rawTx = transaction.serialize();
const sig = await connection.sendRawTransaction(rawTx);
await connection.confirmTransaction(sig, 'confirmed');
return sig;
} catch (error) {
console.log('ERROR In Buy', error);
}
};
```
## Execute Sale
As soon as the buyer and the seller have both posted their bids and offers respectively, and there is a price-match, a Partial or a Complete Order Fulfilment takes place.
Partial or Complete Order fulfilment takes place by calling the `executeSale` instruction.
Its meant to be called by a permission-less crank by `programAsSigner` PDA account that monitors bids and offers on auction house accounts.
The buyer's escrow account will transfer the tokens to the seller (minus auction house fees). The seller's token account will send the NFT to the buyer via a delegate transfer, completing a trade
This is how you can Execute Sale using JS:
```tsx
export const executeSale = async (
connection: anchor.web3.Connection,
buyer: anchor.Wallet,
mint: anchor.web3.PublicKey
) => {
try {
const [programAsSigner, programAsSignerBump] =
await getAuctionHouseProgramAsSigner();
const [escrowPaymentAccount, escrowBump] = await getAuctionHouseBuyerEscrow(
new anchor.web3.PublicKey(AuctionHouse.address),
buyer.publicKey
);
const tokenAccountPubkey = (await getAtaForMint(mint, wallet.publicKey))[0];
const buyerATA = await getOrCreateAssociatedTokenAccount(
connection,
buyer.payer,
mint,
buyer.publicKey
);
const NFTMintData = await getMint(connection, mint);
const tokenSizeAdjusted = new anchor.BN(
await getPriceWithMantissa(1, NFTMintData.decimals)
);
const [freeTradeState, freeTradeBump] = await getAuctionHouseTradeState(
new anchor.web3.PublicKey(AuctionHouse.address),
wallet.publicKey,
tokenAccountPubkey,
new anchor.web3.PublicKey(AuctionHouse.mint),
mint,
tokenSizeAdjusted,
new anchor.BN(0)
);
const TreasuryMintData = await getMint(
connection,
new anchor.web3.PublicKey(AuctionHouse.mint)
);
const buyPriceAdjusted = new anchor.BN(
await getPriceWithMantissa(1, TreasuryMintData.decimals)
);
const [SellertradeState, SellertradeBump] = await getAuctionHouseTradeState(
new anchor.web3.PublicKey(AuctionHouse.address),
wallet.publicKey,
tokenAccountPubkey,
new anchor.web3.PublicKey(AuctionHouse.mint),
mint,
tokenSizeAdjusted,
buyPriceAdjusted
);
const [BuyertradeState, BuyertradeBump] = await getAuctionHouseTradeState(
new anchor.web3.PublicKey(AuctionHouse.address),
buyer.publicKey,
tokenAccountPubkey,
new anchor.web3.PublicKey(AuctionHouse.mint),
mint,
tokenSizeAdjusted,
buyPriceAdjusted
);
const accounts: ExecuteSaleInstructionAccounts = {
treasuryMint: new anchor.web3.PublicKey(AuctionHouse.mint),
auctionHouse: new anchor.web3.PublicKey(AuctionHouse.address),
auctionHouseFeeAccount: new anchor.web3.PublicKey(
AuctionHouse.feeAccount
),
authority: new anchor.web3.PublicKey(AuctionHouse.authority),
programAsSigner: programAsSigner,
auctionHouseTreasury: new anchor.web3.PublicKey(
AuctionHouse.treasuryAccount
),
seller: wallet.publicKey,
metadata: await getMetadata(mint),
tokenMint: mint,
buyer: buyer.publicKey,
escrowPaymentAccount: escrowPaymentAccount,
sellerPaymentReceiptAccount: wallet.publicKey,
tokenAccount: tokenAccountPubkey,
buyerReceiptTokenAccount: buyerATA.address,
freeTradeState: freeTradeState,
sellerTradeState: SellertradeState,
buyerTradeState: BuyertradeState
};
const args: ExecuteSaleInstructionArgs = {
buyerPrice: buyPriceAdjusted,
escrowPaymentBump: escrowBump,
tokenSize: tokenSizeAdjusted,
freeTradeStateBump: freeTradeBump,
programAsSignerBump: programAsSignerBump
};
const instruction = createExecuteSaleInstruction(accounts, args);
const { blockhash } = await connection.getLatestBlockhash();
const transaction = new anchor.web3.Transaction({
recentBlockhash: blockhash
});
transaction.add(instruction);
wallet.signTransaction(transaction);
const rawTx = transaction.serialize();
const sig = await connection.sendRawTransaction(rawTx);
await connection.confirmTransaction(sig, 'confirmed');
return sig;
} catch (error) {
console.log('ERROR In Execute Sale', error);
}
};
```
## Other Instructions
* Cancel - Potential buyer revokes their offer.
* Show Escrow - Print out the balance of an auction house escrow account for a given wallet.
* Withdraw - Transfer funds from user's buyer escrow account for the auction house to their wallet.
* Deposit - Add funds to user's buyer escrow account for the auction house.
* Withdraw from Fee - Transfer funds from auction house fee wallet to the auction house authority.
* Widthraw from Treasury - Transfer funds from the auction house treasury wallet to the auction house authority.
* Update Auction House - Update any of the auction house settings including it's authority or seller fee.
## Auction House Receipts
To aid transaction tracking, Auction House supports the generation of receipts for listings, bids, and sales.
To generate these receipts, the receipt printing function should be called immediately after the corresponding transaction: `PrintListingReceipt`, `PrintBidReceipt`, and `PrintPurchaseReceipt`
Additionally, the `CancelListingReceipt` and `CancelBidReceipt` instructions should be called in the case of canceled listing add bids. Calling these two instructions will fill the `canceled_at` fields of the `ListingReceipt` and `BidReceipt` accounts.
# How to update an Auction House?
The Auction house program has a function to update the attributes value of an Auction House instance that has been already created. Let’s look into how can be update an already existing auction house instance.
Firstly, let’s find the Auction house instance
```tsx
const auction_house_address = new PublicKey(
'Gr31akcY9wb7Gsnu4ej39MydBfxcT8mehZtKxFTsCAai'
);
const ah = await mx
.auctions()
.findAuctionHouseByAddress(auction_house_address)
.run();
```
Now let’s update the authority and the `sellerFeeBasisPoints` points for this auction house instance.
```tsx
const {auctionHouse: updateAuctionHouse} = await mx
.auctions()
.updateAuctionHouse(ah, {
sellerFeeBasisPoints: 300,
newAuthority: newAhAuthority,
})
.run();
```
## What info do MPs need to track?
Specifically Marketplaces should currently store:
* Trade State Account Keys
* Trade State Token Size and Price parts of the seed
* Token Account Keys that are stored in the trade state
* Auction House Receipts (Listing Receipts, Bid Receipts, and Purchase Receipts)
Specifically Marketplaces need to track these two events on Token Accounts:
* Ownership has changed from the original Seller of the NFT
* Token Account Amount has changed to 0
## An important corner case:
What happens if the user makes any change to the token account of the NFT / SPL-tokens (what? spl-tokens can also be auctioned? More on that later) after making the sell order but before executeSale goes through?
Well, quite simply the sale is cancelled (temporarily). So no subsequent call to `executeSale` will be successful until the required amount of tokens are back in the associated token account (ATA). The AH program doesn't do anything other than make the executeSale instruction fail until this happens.
To cancel the sale completely: one needs to manually invoke the `Cancel` instruction to revoke programAsSigner account as the Delegate and wipe the Sell trade state.
## Auctioning Fungible Assets
Auction House can be used not only to create NFT marketplaces but also to create Decentralised Exchanges (DEX).
Auction House has no restrictions on the token type and size that can be auctioned using it. So in practice any SPL-token, fungible, and semi-fungible assets can be auctioned off using the Auction House protocol. A sell order for a token with `tokenSize` > 1 can be made for SPL-tokens or any other fungible assets.
In case of DEXs, with the help of Partial Order Fulfillment, Auction House protocol can be used to replicate the order of actions in traditional DEXs where a user can make a buy order for less than equal to the quantity of tokens offered by other users.
Important to note here is that, as the Auction house does not have or create a central orderbook, the marketplaces or the DEXs will have to keep track of listings, offers and sales going through the given Auction House using receipts.
## Partial Order Fulfilment
Once the seller creates a sell order for a fungible asset with `tokenSize` > 1, the buyer can create a buy order for the said assets with `tokenSize` <= to the `tokenSize` in the sell order.
In order for `ExecuteSale` to succeed, the buy order must have been created with both a `partial_order_size` and a `partial_order_price`. `partial_order_size` must not be greater than the total amount of tokens in the original sell order.
If there is no partial order needing to take place, `partital_order_price` and `partial_order_size` can be passed in as `None`.
## Auction House Public Bids
A standard bid, also called a private bid, refers to a bid made that's specific to an auction. When the auction is complete, the bid can be canceled and the funds in escrow returned to the bidder.
However, Auction House also supports public bids which are specific to the token itself and not to any specific auction. This means that a bid can stay active beyond the end of an auction and be resolved if it meets the criteria for subsequent auctions of that token.
For a fungible asset, thus, there can be multiple partial order fulfillments for a single sell order. While for NFTs, the same asset can be bid upon AND sold even after subsequent sales go through.
## Auctioneer
The current AH implementation is designed with instant sales in mind and currently has no features that enable the various auction types that have become popular in the Solana ecosystem (ex. Timed auctions).
Auctioneer is a customized contract type, written by the user, that uses the composability pattern of AH to control an individual Auction House instance.
To fully enable Auctioneer to use an Auction House instance's instructions, it must be explicitly delegated. The `DelegateAuctioneer` command is used to tell the Auction House instance which program will be using the `auctioneer_\<method>` instructions.
## Auctioneer Scope
By default, the Auctioneer contract has control over the following Auction House methods via the `auctioneer_\<method>` instructions.
* Buy
* Public Buy
* Sell
* Execute Sale
* Cancel
* Deposit
* Withdraw
## References
* Metaplex docs: https://docs.metaplex.com/auction-house/definition
* Prof Lupin's Auction House guide: https://proflupin.xyz/metaplex-auction-house
* Jordan's twitter thread: https://twitter.com/redacted_j/status/1453926144248623104
* 0xRohan's twitter thread: https://twitter.com/0xrohan/status/1458364632888844288
* Armani's twitter thread: https://twitter.com/armaniferrante/status/1460760940454965248
* arcticmatt's blog: https://github.com/arcticmatt/blog/tree/wip/solana/auction-house
* Metaplex documentation: https://docs.metaplex.com/guides/auction-house/auctioneer
Visit: https://twitter.com/0xprof_lupin