This article took longer than expected, and I struggled at almost every step for different reasons. But… stop crying, let's make some code =) 1. Introduction In this article, we dive into with NTFs, and because simply creating a smart contract with an NFT and deploying it is very dull and has already been partly in previous articles on this topic, I decided to complicate my life and make these same NFTs created from a bot in Telegram. web3 2. The boring part that everyone skips An NFT, or Non-Fungible Token, is a unique digital asset that represents ownership of a specific item or piece of content, such as artwork, music, videos, or other forms of creative work, using blockchain technology or, in simple words, a link to your asset (storing the asset itself on the blockchain is costly). Here are some critical aspects of NFTs: : NFTs provide a way for creators to sell digital items with a clear record of ownership. This ownership is secured and verified using blockchain technology, typically on the Ethereum blockchain. Digital Ownership : Each NFT has unique properties that cannot be replicated. This uniqueness can create scarcity, which can drive up the value of the NFT. Uniqueness and Scarcity : NFTs can often be used across different blockchain platforms and applications. This means a digital artwork tokenized as an NFT can be viewed, bought, or sold in various virtual environments and marketplaces. Interoperability : The blockchain ledger records the history of each NFT, including its creation, sale, and previous ownership. This transparency helps verify the authenticity and provenance of the digital asset. Provenance and Authenticity : NFTs can be programmed to provide ongoing royalties to their original creators. Whenever the NFT is sold to a new owner, a percentage of the sale can automatically go back to the original creator. Royalties Because it is expensive to store assets in the blockchain, and there is a strong desire to show off distributed technologies, hipsters reinvent torrents and call it IPFS. IPFS, which stands for InterPlanetary File System, is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. IPFS was created to make the web faster, safer, and more open. Here are some key features and concepts associated with IPFS: : Unlike traditional centralized file storage systems, IPFS is built on a distributed network. This means files aren't stored in a single location but distributed across multiple network nodes. Decentralized and Distributed : IPFS uses content-based addressing rather than location-based addressing. Each file and piece of content on IPFS is identified by a unique fingerprint called a cryptographic hash. This ensures that users are accessing the content they requested, as the hash of the content verifies its integrity. Content Addressing : Since IPFS retrieves files from the nearest node rather than a central server, it can lead to faster download speeds and reduced load on any single server. This also makes the system more resilient to censorship and server failures. Efficient and Resilient : IPFS supports versioning and linking, which allows for the creating of complex webs of data with rich relationships and history. Versioning and Linking : IPFS has a wide range of applications, including decentralized web projects, blockchain data storage, file sharing, and as a replacement for traditional HTTP in specific scenarios. Use Cases : Its decentralized nature makes IPFS a popular choice for storing data in blockchain applications, enhancing storage capacity and access efficiency. Compatibility with Blockchain IPFS is part of a more significant movement towards a decentralized internet, where users have more control over their data and how it functions. It is often associated with decentralized technologies like blockchain and distributed ledger technology. 3. Principles of work How will our bot work? When a user sends a message or image, we check if the user added his Ethereum address. We unobtrusively ask you to add it if he hasn't added it. Otherwise, we won’t let you create an NFT. After we have an address, the user can send us an image, which will be a future NFT. When we got the image, we uploaded it into IPFS and created a metafile with information about NFT. This file will be uploaded into IPFS. After we upload everything, we mint NFT in the contract with a link to a metadata file. Say, user, that's all done. … Profit 4. Prerequisites First, we need an account on Telegram Second, create a bot using @BotFather Init app with next app (not that it's essential, and you can use express, but I make some space for the future) npx create-next-app@latest telegram-bot --use-npm Go to folder cd telegram-bot And init hardhat npx hardhat init And we have a basis for our project.. So clean and nice Create file and add into it .env NEXT_TELEGRAM_TOKEN=6440257800:AAGY... NEXT_SEED=qwerty NEXT_VERIFIER_ADDRESS= NEXT_NFT_ADDRESS= NEXT_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 NEXT_JSON_RPC=http://127.0.0.1:8545/ NEXT_CHAIN_ID=1337 NEXT_IPFS_NODE=http://127.0.0.1:5001/ Here is the credential for local development. can be taken from @BotFather NEXT_TELEGRAM_TOKEN 5. Contracts 5.1 Verification Here is a simple key-value storage Create file with the following content contracts/Verification.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.23; // Uncomment this line to use console.log // import "hardhat/console.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract Verification is Ownable { mapping(uint => address) public verified; constructor() Ownable(msg.sender) {} function verify(uint256 _id, address _address) public onlyOwner { verified[_id] = _address; } } and scripts/deploy_verification.js const hre = require("hardhat"); async function main() { const instance = await hre.ethers.deployContract("Verification"); await instance.waitForDeployment(); console.log(`Verification Contract deployed to ${await instance.getAddress()}`); } main().catch((error) => { console.error(error); process.exitCode = 1; });4.2 Nft Deploy it npx hardhat run scripts/deploy_verification.js --network local Save contract address into file .env ... NEXT_VERIFIER_ADDRESS=... ... 5.2 NFT And here I decided not to bother and created a file through the wizard . It’s very convenient, but first, I would still recommend that you practice writing such contracts yourself. https://docs.openzeppelin.com/contracts/5.x/wizard Create file with the following content. contracts/TelegramBot.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract TelegramBot is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable { uint256 private _nextTokenId; constructor(address initialOwner) ERC721("TelegramBot", "TGB") Ownable(initialOwner) {} function safeMint(address to, string memory uri) public onlyOwner { uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); } // The following functions are overrides required by Solidity. function _update(address to, uint256 tokenId, address auth) internal override(ERC721, ERC721Enumerable) returns (address) { return super._update(to, tokenId, auth); } function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) { super._increaseBalance(account, value); } function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) { return super.tokenURI(tokenId); } function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable, ERC721URIStorage) returns (bool) { return super.supportsInterface(interfaceId); } } and scripts/deploy_nft.js const hre = require("hardhat"); async function main() { const signers = await hre.ethers.getSigners(); console.log("Deploying contracts with the account:", signers[0].address); const instance = await hre.ethers.deployContract("TelegramBot", [ signers[0].address, ]); await instance.waitForDeployment(); console.log(`TelegramBot Contract deployed to ${await instance.getAddress()}`); } main().catch((error) => { console.error(error); process.exitCode = 1; }); Deploy it npx hardhat run scripts/deploy_nft.js --network local Save the contract address into file .env ... NEXT_NFT_ADDRESS=... ... 6. Telegram webhook For local testing, I strongly recommend you to use ngrok 6.1 Preparement. Create with the following content. app/api/telegram/route.ts import { TelegramRequest } from "@/app/types"; import { ok } from "@/app/helpers/ok"; export async function POST(request: Request) { const reqData = (await request.json()) as TelegramRequest; console.log(reqData); return ok(); } app/helpers/ok.ts import { NextResponse } from "next/server"; export function ok() { return new NextResponse("Ok", { status: 200, statusText: "Ok", headers: { "Content-Type": "text/plain", }, }); } helpers for interact with Telegram: app/helpers/tg.ts const sendMessageUrl = `https://api.telegram.org/bot${process.env.NEXT_TELEGRAM_TOKEN}/sendMessage`; const fileUrl = `https://api.telegram.org/bot${process.env.NEXT_TELEGRAM_TOKEN}/getFile?file_id=`; const downloadFileUrl = `https://api.telegram.org/file/bot${process.env.NEXT_TELEGRAM_TOKEN}/`; export async function sendMessage(chat_id: String | number, message: String) { const req = await fetch(sendMessageUrl, { method: "POST", body: JSON.stringify({ chat_id: chat_id, text: message, parse_mode: "markdown", }), headers: { "Content-Type": "application/json", }, }); const resp = await req.json(); console.log("sendMessage", resp); return resp; } export async function getFile(file_id: String) { const req = await fetch(`${fileUrl}${file_id}`, { method: "GET", }); const resp = await req.json(); console.log("getFile", resp); return resp; } export async function downloadFile(file_path: String) { const req = await fetch(`${downloadFileUrl}${file_path}`, { method: "GET", }); return await req.blob(); } Helpers for interact with IPFS: app/helpers/ipfs.ts export async function uploadFile( filename: string, data: Blob, ): Promise<{ Name: string; Hash: string; Size: string }> { const form = new FormData(); form.append(filename, data, filename); const uploadResponse = await fetch( `${process.env.NEXT_IPFS_NODE}api/v0/add?pin=true`, { method: "POST", body: form, headers: { "accept-type": "application/json", }, }, ); const resp = await uploadResponse.json(); console.log(`upload ${filename} response:`, resp); return resp; } app/types.ts export type TelegramPhotoRequest = { file_id: string; file_unique_id: string; file_size: number; width: number; height: number; }; export type TelegramEntityRequest = { offset: number; length: number; type: string; }; export type TelegramRequest = { update_id: number; message: { message_id: number; from: { id: number; is_bot: boolean; first_name: string; last_name: string; username: string; language_code: string; is_premium: boolean; }; chat: { id: number; first_name: string; last_name: string; username: string; type: string; }; date: number; text: string; photo: TelegramPhotoRequest[] | undefined; entities: TelegramEntityRequest[] | undefined; caption: string | undefined; }; }; Next steps will be in file app/api/telegram/route.ts 6.2 Initialise provider for work with Ethereum network. import { ethers, JsonRpcProvider } from "ethers"; import { TelegramRequest } from "@/app/types"; import { ok } from "@/app/helpers/ok"; export async function POST(request: Request) { const reqData = (await request.json()) as TelegramRequest; console.log(reqData); const provider = new JsonRpcProvider(); const signer = new ethers.Wallet( process.env.NEXT_PRIVATE_KEY as string, provider, ); return ok(); } 6.3 Initialise verification contract ... const verifierAbi = require("@/artifacts/contracts/Verification.sol/Verification.json").abi; export async function POST(request: Request) { ... const verifierContract = new ethers.Contract( process.env.NEXT_VERIFIER_ADDRESS as string, verifierAbi, signer, ); return ok(); } 6.4 Check user is registered. We need to make a hash of the user ID to preserve his anonymity. As we already know, the blockchain remembers everything, and it will not be very good for the user if we store in clear form a link between the user’s account and his telegram identifier. ... import * as crypto from "crypto"; ... export async function POST(request: Request) { ... const hash = crypto .createHash("sha256") .update(`${process.env.NEXT_SEED}:${reqData.message.from.id}`) .digest(); let address = null; try { address = await verifierContract.verified("0x" + hash.toString("hex")); } catch (e) { console.log("address", e); } if (!address || address === "0x0000000000000000000000000000000000000000") { } return ok(); } 6.5 Registration. If the user is not registered yet, we check the message. If it is the correct address - make registration; otherwise, send a message that we need his address. ... import { isAddress } from "ethers"; import { ok } from "@/app/helpers/ok"; import { sendMessage } from "@/app/helpers/tg"; ... export async function POST(request: Request) { ... if (!address || address === "0x0000000000000000000000000000000000000000") { if (isAddress(reqData.message.text)) { let r = await verifierContract.verify( "0x" + hash.toString("hex"), reqData.message.text.trim(), ); console.log("resp", r); let address = await verifierContract.verified( "0x" + hash.toString("hex"), ); const resp = await sendMessage( reqData.message.chat.id, `${address} registered\n now you can send NFTs`, ); console.log("resp 1", resp); return ok(); } const resp = await sendMessage(reqData.message.chat.id, "need authorize"); console.log("resp 2", resp); return ok(); } return ok(); } 6.6 Validate photo. If user doesn’t sent to us a photo, we need to talk about it ... export async function POST(request: Request) { ... if (!reqData.message.photo) { const resp = await sendMessage(reqData.message.chat.id, `not found photos`); console.log("resp validate", resp); return ok(); } return ok(); } 6.7 Download photo. If all checks are passed, download the sent photo import { downloadFile, getFile } from "@/app/helpers/tg"; export async function POST(request: Request) { ... let photo = reqData.message.photo[reqData.message.photo.length - 1]; let file = await getFile(photo.file_id); console.log("file", file); let data = await downloadFile(file.result.file_path); return ok(); } 6.8 Save to IPFS. ... import { uploadFile } from "@/app/helpers/ipfs"; ... export async function POST(request: Request) { ... const respNft = await uploadFile("image", data); console.log("respNft", respNft); const respMetadata = await uploadFile( "metadata.json", new Blob( [ JSON.stringify({ name: reqData.message.caption, image: `ipfs://${respNft.Hash}`, }), ], { type: "application/json" }, ), ); return ok(); } 6.9 mint users NFT. After we save all data into IPFS, we can mint NFT. const nftAbi = require("@/artifacts/contracts/TelegramBot.sol/TelegramBot.json").abi; export async function POST(request: Request) { ... const nftContract = new ethers.Contract( process.env.NEXT_NFT_ADDRESS as string, nftAbi, signer, ); const mint = await nftContract.safeMint( address, `ipfs://${respMetadata.Hash}`, ); console.log("mint", mint); const respMint = await sendMessage( reqData.message.chat.id, `tx hash: ${mint.tx_hash}`, ); console.log("resp mint", respMint); return ok(); } Done =) 7. Conclusion This is the simplest possible implementation, which has some usability problems but is enough to understand the principles of operation. There are plans to make a web version using Metamask, which NEXT was used for. I tried not to use libraries to communicate with Telegram and IPFS so that there would be more understanding of how it works and you can install the library very quickly. I hope this will be enough to understand the principles of operation. The source code is on . GitHub