How to Build and Deploy a Subgraph for Polygon zkEVM using Alchemy Subgraphs
Learn how to build and depoy a subgraph for Polygon zkEVM that indexes ERC-721 transfer events using Alchemy Subgraphs.
Introduction
In this guide, we will walk you through the process of creating a simple subgraph hosted on the Alchemy Subgraphs platform and deployed for Polygon zkEVM. The subgraph will be a simple one that indexes an ERC-721 contract's Transfer events - feel free to copy this same exact flow with a contract of your choice.
Developer Environment
Before proceeding with the guide, ensure you meet the following requirements:
- Node.js: You must have Node.js version 18 or higher installed on your system. If you do not have Node.js installed, or if your version is lower than 18, you can download the latest version from the official website.
- Run
node -v
to verify.
- Run
- Install the Graph CLI (graph-cli) NPM package globally by running
npm i -g @graphprotocol/[email protected]
.
Step 1: Run the Subgraph CLI Wizard
Let's set up a local project to put together all the files that will be used to build and deploy our subgraph:
-
Navigate to a directory where you want to initialize your project
-
Make sure you have the Graph CLI (graph-cli) NPM package installed globally. Run
npm i -g @graphprotocol/[email protected]
- Run
graph
to verify you have this package installed
- Run
-
Run
graph init
to initialize the package's CLI wizard and select the following options:-
Protocol:
ethereum
-
Product for which to initialize:
hosted-service
-
Subgraph name:
alchemy/zkevm-erc721-transfers
-
Directory to create the subgraph in:
zkevm-subgraphs
-
Ethereum network:
polygon-zkevm
-
Contract address: 0x5A3b2E7f335be432f834b3F1bfEf19B44d1f310C
-
ABI: We are using The Graph's CLI to initialize the project and it doesn't support Polygon zkEVM yet so automatic fetching of ABI will fail and you will be asked if you want to retry, just go with
n
, we will do this manually.
Create a new json file in your current directory where you are running the command namedERC721.json
and add the following content to it (this is the ABI of our contract fetched manually from Polygonscan):[ { "inputs": [ { "internalType": "address", "name": "_endpoint", "type": "address" }, { "internalType": "uint256", "name": "_startTokenId", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [], "name": "InsufficientGas", "type": "error" }, { "inputs": [], "name": "NotTokenOwner", "type": "error" }, { "inputs": [], "name": "SupplyExceeded", "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "approved", "type": "address" }, { "indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "operator", "type": "address" }, { "indexed": false, "internalType": "bool", "name": "approved", "type": "bool" } ], "name": "ApprovalForAll", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "indexed": false, "internalType": "bytes", "name": "_srcAddress", "type": "bytes" }, { "indexed": false, "internalType": "uint64", "name": "_nonce", "type": "uint64" }, { "indexed": false, "internalType": "bytes", "name": "_payload", "type": "bytes" }, { "indexed": false, "internalType": "bytes", "name": "_reason", "type": "bytes" } ], "name": "MessageFailed", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "indexed": false, "internalType": "address", "name": "_from", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "_tokenId", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "counter", "type": "uint256" } ], "name": "ReceivedNFT", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "indexed": false, "internalType": "bytes", "name": "_srcAddress", "type": "bytes" }, { "indexed": false, "internalType": "uint64", "name": "_nonce", "type": "uint64" }, { "indexed": false, "internalType": "bytes32", "name": "_payloadHash", "type": "bytes32" } ], "name": "RetryMessageSuccess", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint16", "name": "_dstChainId", "type": "uint16" }, { "indexed": false, "internalType": "uint16", "name": "_type", "type": "uint16" }, { "indexed": false, "internalType": "uint256", "name": "_minDstGas", "type": "uint256" } ], "name": "SetMinDstGas", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "address", "name": "precrime", "type": "address" } ], "name": "SetPrecrime", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint16", "name": "_remoteChainId", "type": "uint16" }, { "indexed": false, "internalType": "bytes", "name": "_path", "type": "bytes" } ], "name": "SetTrustedRemote", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint16", "name": "_remoteChainId", "type": "uint16" }, { "indexed": false, "internalType": "bytes", "name": "_remoteAddress", "type": "bytes" } ], "name": "SetTrustedRemoteAddress", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "Transfer", "type": "event" }, { "inputs": [], "name": "DEFAULT_PAYLOAD_SIZE_LIMIT", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "MAX_ID", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "approve", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "counter", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "dstChainId", "type": "uint16" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "crossChain", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "currentTokenId", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "dstChainId", "type": "uint16" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "estimateFees", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "", "type": "uint16" }, { "internalType": "bytes", "name": "", "type": "bytes" }, { "internalType": "uint64", "name": "", "type": "uint64" } ], "name": "failedMessages", "outputs": [ { "internalType": "bytes32", "name": "", "type": "bytes32" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "internalType": "bytes", "name": "_srcAddress", "type": "bytes" } ], "name": "forceResumeReceive", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "getApproved", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_version", "type": "uint16" }, { "internalType": "uint16", "name": "_chainId", "type": "uint16" }, { "internalType": "address", "name": "", "type": "address" }, { "internalType": "uint256", "name": "_configType", "type": "uint256" } ], "name": "getConfig", "outputs": [ { "internalType": "bytes", "name": "", "type": "bytes" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_remoteChainId", "type": "uint16" } ], "name": "getTrustedRemoteAddress", "outputs": [ { "internalType": "bytes", "name": "", "type": "bytes" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "operator", "type": "address" } ], "name": "isApprovedForAll", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "internalType": "bytes", "name": "_srcAddress", "type": "bytes" } ], "name": "isTrustedRemote", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "lzEndpoint", "outputs": [ { "internalType": "contract ILayerZeroEndpoint", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "internalType": "bytes", "name": "_srcAddress", "type": "bytes" }, { "internalType": "uint64", "name": "_nonce", "type": "uint64" }, { "internalType": "bytes", "name": "_payload", "type": "bytes" } ], "name": "lzReceive", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "", "type": "uint16" }, { "internalType": "uint16", "name": "", "type": "uint16" } ], "name": "minDstGasLookup", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "mint", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "name", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "internalType": "bytes", "name": "_srcAddress", "type": "bytes" }, { "internalType": "uint64", "name": "_nonce", "type": "uint64" }, { "internalType": "bytes", "name": "_payload", "type": "bytes" } ], "name": "nonblockingLzReceive", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "ownerOf", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "", "type": "uint16" } ], "name": "payloadSizeLimitLookup", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "precrime", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "internalType": "bytes", "name": "_srcAddress", "type": "bytes" }, { "internalType": "uint64", "name": "_nonce", "type": "uint64" }, { "internalType": "bytes", "name": "_payload", "type": "bytes" } ], "name": "retryMessage", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "safeTransferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], "name": "safeTransferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "operator", "type": "address" }, { "internalType": "bool", "name": "approved", "type": "bool" } ], "name": "setApprovalForAll", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_version", "type": "uint16" }, { "internalType": "uint16", "name": "_chainId", "type": "uint16" }, { "internalType": "uint256", "name": "_configType", "type": "uint256" }, { "internalType": "bytes", "name": "_config", "type": "bytes" } ], "name": "setConfig", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_dstChainId", "type": "uint16" }, { "internalType": "uint16", "name": "_packetType", "type": "uint16" }, { "internalType": "uint256", "name": "_minGas", "type": "uint256" } ], "name": "setMinDstGas", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_dstChainId", "type": "uint16" }, { "internalType": "uint256", "name": "_size", "type": "uint256" } ], "name": "setPayloadSizeLimit", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "_precrime", "type": "address" } ], "name": "setPrecrime", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_version", "type": "uint16" } ], "name": "setReceiveVersion", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_version", "type": "uint16" } ], "name": "setSendVersion", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_srcChainId", "type": "uint16" }, { "internalType": "bytes", "name": "_path", "type": "bytes" } ], "name": "setTrustedRemote", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "_remoteChainId", "type": "uint16" }, { "internalType": "bytes", "name": "_remoteAddress", "type": "bytes" } ], "name": "setTrustedRemoteAddress", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" } ], "name": "supportsInterface", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "symbol", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "tokenURI", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], "name": "transferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint16", "name": "", "type": "uint16" } ], "name": "trustedRemoteLookup", "outputs": [ { "internalType": "bytes", "name": "", "type": "bytes" } ], "stateMutability": "view", "type": "function" } ]
-
Start block: Due to the same reason above fetching the start block will also fail, and you will be asked if you want to retry, just go with
n
again, we will do this manually. -
ABI file (path):
ERC721.json
(file path of your ABI that you just created) -
Start Block:
409341
(manually fetched the block number from Polygonscan in which this contract was deployed) -
Contract name:
ERC721
-
Index contract events as entities (Y/n):
y
(press Enter) -
Once the wizard process begins, you will be asked if you want to add another contract:
n
-
The wizard will now finish the local subgraph build process. Once done, your terminal should look like this:
Step 2: Set Up Your Local Files
If you don't want to create a simplified version of this subgraph below, you are ready to deploy your subgraph to the Alchemy Subgraphs platform. Go to Step #4 if you want to skip the deep dive into the files of the project below.
First of all, make sure to navigate to the newly created project you just set up with Step #1:
- Run
cd zkevm-subgraphs
- Open the folder in your preferred IDE and get familiar with the project files:
When building subgraphs, there are three main files you want to focus on before everything works properly:
schema.graphql
: This file defines the GraphQL schema for your subgraph. The GraphQL schema is basically the data blueprint of what you want to index from the blockchain and in what format. The schema is used to define entities, which are used to generate the database and the GraphQL API, enabling developers to perform queries on the indexed data. In blockchain subgraph land, typically entities are just what contract Events you want to index and how.
Entities are simply interfaces for the data you want to index. For example, if you are indexing NFT transfers, you can have an entity named
Transfer
with fields such as id, fromAddress, toAddress, tokenId, and timestamp.
subgraph.yaml
(also referred to as the subgraph manifest): This file contains all of the key data regarding how the subgraph should be built. It references the entities defined in theschema.graphql
. If you open the file in your local project, you'll notice it contains a lot of what you filled in during the wizard process like the contractaddress
andstartBlock
. The manifest basically aggregates all of of your subgraph's most important data into one file.- the
.ts
file inside/src
that was auto-generated by the wizard process (if you are following this guide, it is calledpudgy-penguins.ts
): This file contains the function handlers for your entities. When your subgraph indexes a new event, it will run the function, defined in this file, (and mapped to the entity in thesubgraph.yaml
file) to that entity.
Notice: the wizard process creates a
/abis
folder and automatically populates the contractabi
, which you'll need. The file is calledPudgyPenguins.json
in this guide project.
Ok, now that we've defined what the most important files in our project are, let's continue with the steps from above...
The wizard process, by default, sets you up with a fully loaded project correspondent to the smart contract you loaded. This means it will read the contract's ABI and create an entity for each of the contract's defined events. If you open the project's schema.graphql
file, you'll notice all of the Pudgy Penguin NFT contract events defined as entities. This is great if you're building a wide-range app specific to Pudgy Penguins, but for our purposes let's keep things simple and only work with one entity: the Transfer
entity.
- Open the
schema.graphql
file and remove every entity except theTransfer
entity. You can overwrite the file and copy-paste the following:
type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes! # address
to: Bytes! # address
tokenId: BigInt! # uint256
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
- Save and close the file.
- Now, open the
subgraph.yaml
file.
We want to remove everything that is not specific to the Transfer
event of the Pudgy Penguins smart contract.
- Remove any mentions of entities that aren't Transfer or overwrite the file and copy-paste the following:
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: ERC721
network: polygon-zkevm
source:
address: "0x5A3b2E7f335be432f834b3F1bfEf19B44d1f310C"
abi: ERC721
startBlock: 409340
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Transfer
abis:
- name: ERC721
file: ./abis/ERC721.json
eventHandlers:
- event: Transfer(indexed address,indexed address,indexed uint256)
handler: handleTransfer
file: ./src/erc-721.ts
Notice, these are the same exact file contents as the boilerplate but we are just removing anything not specific to the
Transfer
entity.
- Save and close the file.
- Now, open the
erc-721.ts
file in the/src
folder. Remove any function handlers non-specific to theTransfer
event or overwrite the file and copy-paste the following:
import { Transfer as TransferEvent } from "../generated/ERC721/ERC721";
import { Transfer } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let entity = new Transfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.from = event.params.from;
entity.to = event.params.to;
entity.tokenId = event.params.tokenId;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
}
- Save and close the file. If you manually removed the function handlers, make sure to also remove any of the unused imports.
Step 3: Build Your Subgraph
Now that we've got all of the important files sorted out, let's build our subgraph artifacts in order to get them ready to be deployed to the Alchemy Subgraphs platform! 🕺
- In your project's root folder, run
graph codegen
(this will re-build and overwrite all of the boilerplate build files with our more refined and efficient subgraph specs specific to our NFT Transfers)
Your terminal should output the following:
When you run the command graph codegen
and see the message Types generated successfully
in your terminal, it means that The Graph CLI has successfully generated the necessary code based on your subgraph's GraphQL schema (defined in the schema.graphql
file) and the ABIs of the smart contracts specified in your subgraph manifest (defined in the subgraph.yaml
file). Overall, the command ensures that your mapping functions (defined in src/erc-721.ts
) can interact with entity types in a type-safe manner.
- Finally, run
graph build
Your terminal should now output the following:
When you run the command graph build
, your subgraph files are compiled, essentially acting as a final build check making sure that your schemas, manifest, and mappings are all correct and compatible. Without this final step, you will not be able to deploy your subgraph to the Alchemy Subgraphs platform.
Step 4: Deploy Your Subgraph to Alchemy Subgraphs
This step is the easiest. 🥞 You'll now deploy the subgraph you built and compiled locally to be hosted on the Alchemy Subgraphs platform:
- Acquire your unique
deploy-key
from the Alchemy Subgraphs Dashboard (you will need to log in with your Alchemy account)- You can use the
default
key provided to you or hit+ Create Query Key
to create a new one.
- You can use the
- Plug in your
deploy-key
where it saysCOPY_PASTE_YOUR_DEPLOY_KEY_HERE
and then run the following in your terminal (paste it all as one command!):
graph deploy zkevm-erc721-transfers \
--version-label v0.0.1-new-version \
--node https://subgraphs.alchemy.com/api/subgraphs/deploy \
--deploy-key COPY_PASTE_YOUR_DEPLOY_KEY_HERE \
--ipfs https://ipfs.satsuma.xyz
Note: If you followed this guide exactly, you might get an output that indicates this subgraph has already been deployed!
Your terminal should output the following: 👀
The key line you want to see in your terminal is: Deployed to link.
You will need to sign in using your Alchemy account to view the subgraph dashboard!
When you visit the link (which you can share with your community or team of developers), you will get a dashboard loaded with all of the details you'll need about your newly-deployed subgraph:
NOTE
If you get a "Network not supported" error please reach out to us to get Polygon zkEVM enabled for your account.
Congrats! 🎉 You've just fully built a customized subgraph project and used it to deploy a live subgraph onto the Alchemy Subgraphs platform! ✅
Updated 5 months ago