DEV Community

Cover image for Bot de Monitoramento para SushiSwap V3 em Go — Parte 1
Cláudio Filipe Lima Rapôso
Cláudio Filipe Lima Rapôso

Posted on

Bot de Monitoramento para SushiSwap V3 em Go — Parte 1

Este guia descreve, de forma contínua e didática, como construir a primeira metade de um bot de trading em Go. Nesta etapa não há transações on-chain. O programa observa o preço em dólar de um token de referência, compara esse valor com limites configurados em um arquivo .env e decide, de maneira simulada, quando compraria e quando venderia. O objetivo é validar lógica e parâmetros sem risco financeiro, além de introduzir a persistência do estado em SQLite para que o bot possa ser interrompido e retomado sem perder contexto.

Conceito e fluxo

Arquitetura de Referência

O comportamento é simples. O aplicativo carrega as configurações do .env, consulta periodicamente o preço em USD do token de referência por HTTP e imprime no console o valor atual. Se o preço observado estiver abaixo ou igual ao alvo de compra e ainda não houver posição aberta, considera-se uma entrada e o estado é salvo no banco com a informação do preço de entrada. Se, com posição aberta, o preço atingir o alvo de lucro, considera-se a saída e o estado é atualizado para indicar que não há mais posição. Em qualquer outro caso, o programa apenas aguarda. Como não há transações nesta fase, tratamos essas decisões como um simulador de gatilhos, útil para ajustar parâmetros e entender o ciclo de execução.


Preparação do ambiente

É necessário ter Go instalado em versão recente. Crie uma pasta para o projeto, inicialize um módulo e instale as dependências. O pacote godotenv carrega variáveis do arquivo .env e o driver modernc.org/sqlite possibilita usar SQLite sem CGO, o que simplifica a portabilidade.

mkdir sushiswap-v3-go && cd sushiswap-v3-go
go mod init example.com/sushiswap-v3-go
go get github.com/joho/godotenv
go get modernc.org/sqlite
Enter fullscreen mode Exit fullscreen mode

Configuração com .env

Crie um arquivo .env na raiz do projeto. Ele concentra variáveis de rede e parâmetros de estratégia. Também indicamos DB_PATH, o caminho do arquivo SQLite que manterá o estado.

CHAIN_ID=11155111
NETWORK=sepolia
INFURA_API_KEY=SUA_INFURA_KEY

TOKEN0_ADDRESS=0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14
TOKEN1_ADDRESS=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238

QUOTE0_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

WALLET=0xSUA_WALLET
PRIVATE_KEY=0xSUA_PRIVATE_KEY

ROUTER_ADDRESS=0x3b0aa7d38bf3c103bf02d1de2e37568cbed3d6e8

PRICE_TO_BUY=5000
AMOUNT_TO_BUY=0.1
PROFITABILITY=1.1

DB_PATH=./bot.db
Enter fullscreen mode Exit fullscreen mode

A variável QUOTE0_ADDRESS representa o token cujo preço em USD será observado; no exemplo, usa-se o WETH da mainnet por ser uma referência líquida e amplamente utilizada. As variáveis PRICE_TO_BUY e PROFITABILITY definem, respectivamente, o alvo de entrada e o multiplicador do alvo de lucro. As chaves de carteira e o endereço do router são armazenados desde já, embora só venham a ser utilizadas na fase seguinte, quando forem adicionadas transações reais. Recomenda-se manter o arquivo .env fora do controle de versão.


Implementação do código

A seguir estão os arquivos Go, organizados por responsabilidade. Basta criar cada arquivo com o conteúdo indicado.

config.go

Este módulo lê e valida o .env, convertendo valores numéricos e garantindo que os campos mínimos existam para a execução desta etapa. Se faltar algo essencial, a função sinaliza claramente o problema.

package main

import (
    "fmt"
    "os"
    "strconv"

    "github.com/joho/godotenv"
)

type Config struct {
    ChainID       int
    Network       string
    InfuraAPIKey  string
    Token0Address string
    Token1Address string
    Quote0Address string
    Wallet        string
    PrivateKey    string
    RouterAddress string
    PriceToBuy    float64
    AmountToBuy   string
    Profitability float64
    DBPath        string
}

func LoadConfig() (*Config, error) {
    _ = godotenv.Load()

    var err error
    cfg := &Config{}

    if v := os.Getenv("CHAIN_ID"); v != "" {
        if cfg.ChainID, err = strconv.Atoi(v); err != nil {
            return nil, fmt.Errorf("CHAIN_ID inválido: %w", err)
        }
    } else {
        cfg.ChainID = 11155111
    }

    cfg.Network = os.Getenv("NETWORK")
    cfg.InfuraAPIKey = os.Getenv("INFURA_API_KEY")
    cfg.Token0Address = os.Getenv("TOKEN0_ADDRESS")
    cfg.Token1Address = os.Getenv("TOKEN1_ADDRESS")
    cfg.Quote0Address = os.Getenv("QUOTE0_ADDRESS")
    cfg.Wallet = os.Getenv("WALLET")
    cfg.PrivateKey = os.Getenv("PRIVATE_KEY")
    cfg.RouterAddress = os.Getenv("ROUTER_ADDRESS")
    cfg.AmountToBuy = os.Getenv("AMOUNT_TO_BUY")

    if v := os.Getenv("PRICE_TO_BUY"); v != "" {
        if cfg.PriceToBuy, err = strconv.ParseFloat(v, 64); err != nil {
            return nil, fmt.Errorf("PRICE_TO_BUY inválido: %w", err)
        }
    } else {
        return nil, fmt.Errorf("PRICE_TO_BUY ausente")
    }

    if v := os.Getenv("PROFITABILITY"); v != "" {
        if cfg.Profitability, err = strconv.ParseFloat(v, 64); err != nil {
            return nil, fmt.Errorf("PROFITABILITY inválido: %w", err)
        }
    } else {
        return nil, fmt.Errorf("PROFITABILITY ausente")
    }

    if cfg.Quote0Address == "" {
        return nil, fmt.Errorf("QUOTE0_ADDRESS ausente")
    }

    if v := os.Getenv("DB_PATH"); v != "" {
        cfg.DBPath = v
    } else {
        cfg.DBPath = "./bot.db"
    }

    return cfg, nil
}
Enter fullscreen mode Exit fullscreen mode

database.go

Este módulo cria e inicializa o banco SQLite, além de oferecer funções para ler e salvar o estado do bot. O estado se restringe a duas informações: se há ou não posição aberta e qual foi o preço de entrada, quando aplicável. Ao iniciar pela primeira vez, uma linha padrão é criada para garantir que o acesso seja simples e previsível nas execuções subsequentes.

package main

import (
    "database/sql"
    "fmt"

    _ "modernc.org/sqlite"
)

type BotState struct {
    IsOpened   bool
    EntryPrice *float64
}

func InitDB(path string) (*sql.DB, error) {
    db, err := sql.Open("sqlite", path)
    if err != nil {
        return nil, fmt.Errorf("erro abrindo sqlite: %w", err)
    }
    schema := `
    CREATE TABLE IF NOT EXISTS bot_state (
        id INTEGER PRIMARY KEY CHECK (id = 1),
        is_opened INTEGER NOT NULL,
        entry_price REAL
    );
    INSERT INTO bot_state (id, is_opened, entry_price)
        SELECT 1, 0, NULL
        WHERE NOT EXISTS (SELECT 1 FROM bot_state WHERE id = 1);
    `
    if _, err := db.Exec(schema); err != nil {
        return nil, fmt.Errorf("erro criando schema: %w", err)
    }
    return db, nil
}

func LoadBotState(db *sql.DB) (*BotState, error) {
    row := db.QueryRow(`SELECT is_opened, entry_price FROM bot_state WHERE id = 1`)
    var isOpenedInt int
    var entryPrice sql.NullFloat64
    if err := row.Scan(&isOpenedInt, &entryPrice); err != nil {
        return nil, fmt.Errorf("erro lendo estado: %w", err)
    }
    var ep *float64
    if entryPrice.Valid {
        v := entryPrice.Float64
        ep = &v
    }
    return &BotState{
        IsOpened:   isOpenedInt == 1,
        EntryPrice: ep,
    }, nil
}

func SaveBotState(db *sql.DB, st *BotState) error {
    var isOpenedInt int
    if st.IsOpened {
        isOpenedInt = 1
    } else {
        isOpenedInt = 0
    }

    var entry interface{}
    if st.EntryPrice != nil {
        entry = *st.EntryPrice
    }

    if _, err := db.Exec(
        `UPDATE bot_state SET is_opened = ?, entry_price = ? WHERE id = 1`,
        isOpenedInt, entry,
    ); err != nil {
        return fmt.Errorf("erro salvando estado: %w", err)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

prices.go

Esta unidade contém a função que consulta a API pública de preços. O retorno esperado é um número de ponto flutuante representando o preço em USD do token indicado. A configuração de timeout e a verificação do status HTTP ajudam a tornar o comportamento previsível.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

func fetchPriceUSD(tokenAddress string) (float64, error) {
    url := fmt.Sprintf("https://api.sushi.com/price/v1/%d/%s", 1, tokenAddress)

    req, _ := http.NewRequest(http.MethodGet, url, nil)
    req.Header.Set("Accept", "application/json")

    client := &http.Client{Timeout: 10 * time.Second}
    res, err := client.Do(req)
    if err != nil {
        return 0, err
    }
    defer res.Body.Close()

    if res.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(res.Body)
        return 0, fmt.Errorf("pricing api status %d: %s", res.StatusCode, string(body))
    }

    var price float64
    if err := json.NewDecoder(res.Body).Decode(&price); err != nil {
        return 0, fmt.Errorf("falha ao decodificar preço: %w", err)
    }
    return price, nil
}
Enter fullscreen mode Exit fullscreen mode

main.go

O arquivo principal integra as partes. Ao iniciar, o programa carrega a configuração, prepara o banco, recupera o estado e entra em um ciclo que consulta o preço, imprime o valor e executa a lógica de decisão, atualizando o SQLite sempre que houver mudanças de estado. Um comando para limpar a tela mantém o console com aparência de painel.

package main

import (
    "fmt"
    "log"
    "time"
)

func monitor(cfg *Config) (float64, error) {
    price, err := fetchPriceUSD(cfg.Quote0Address)
    if err != nil {
        return 0, err
    }
    fmt.Print("\033[2J\033[H")
    fmt.Println("Price:", price)
    return price, nil
}

func main() {
    cfg, err := LoadConfig()
    if err != nil {
        log.Fatalf("config erro: %v", err)
    }

    db, err := InitDB(cfg.DBPath)
    if err != nil {
        log.Fatalf("db erro: %v", err)
    }
    defer db.Close()

    state, err := LoadBotState(db)
    if err != nil {
        log.Fatalf("erro carregando estado: %v", err)
    }

    cycle := func() {
        price, err := monitor(cfg)
        if err != nil {
            log.Printf("erro monitorando preço: %v", err)
            return
        }

        if !state.IsOpened && price <= cfg.PriceToBuy {
            state.IsOpened = true
            state.EntryPrice = &price
            if err := SaveBotState(db, state); err != nil {
                log.Printf("erro salvando estado (swap in): %v", err)
            }
            fmt.Printf("Swap in (entry=%.6f)\n", price)
            return
        }

        target := cfg.PriceToBuy * cfg.Profitability
        if state.IsOpened && price >= target {
            state.IsOpened = false
            state.EntryPrice = nil
            if err := SaveBotState(db, state); err != nil {
                log.Printf("erro salvando estado (swap out): %v", err)
            }
            fmt.Printf("Swap out (target=%.6f)\n", target)
            return
        }

        if state.IsOpened && state.EntryPrice != nil {
            fmt.Printf("Wait... (opened at %.6f, target %.6f)\n", *state.EntryPrice, target)
            return
        }

        fmt.Println("Wait...")
    }

    cycle()
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    for range ticker.C {
        cycle()
    }
}
Enter fullscreen mode Exit fullscreen mode

Execução

Com os arquivos criados e o .env preenchido, execute o projeto a partir da raiz. O console exibirá o preço e a decisão correspondente do ciclo. Quando o preço atingir o alvo de compra, aparecerá a indicação de entrada e o estado será salvo. Quando alcançar o alvo de lucro com posição aberta, a saída será registrada. Se o programa for encerrado e reiniciado, a leitura do banco restaurará a situação anterior.

go run .
Enter fullscreen mode Exit fullscreen mode

Encerramento

Com esta base consolidada, você tem um esqueleto funcional capaz de observar preço, tomar decisões simuladas e manter consistência entre execuções por meio de SQLite. Na Parte 2, em vez de montar e enviar a transação on-chain localmente, o fluxo de swap será executado via gRPC: o cliente em Go fará chamadas a um serviço gRPC responsável por cotar e processar a operação de compra e venda. Esse serviço receberá parâmetros como chain_id, token_in, token_out, amount, max_slippage e wallet, retornará métricas de pré-troca (por exemplo, o assumedAmountOut) e cuidará da preparação/execução da transação, incluindo roteamento, montagem do payload, assinatura e envio, além do retorno do hash e do recibo quando confirmada.

Do ponto de vista de arquitetura, o cliente Go apenas disciplina a orquestração: abre o canal gRPC, envia a requisição de Quote/Execute, aguarda a resposta (com sucesso ou erro tratado) e registra estado/telemetria. O serviço gRPC concentra a lógica sensível: integração com o roteador (para encontrar a melhor rota), aplicação de slippage máxima, políticas de deadline e retries, idempotência por chave, auditoria, observabilidade e segurança de chaves (sem trafegar PRIVATE_KEY pelo cliente). A assinatura pode ficar no servidor (HSM, KMS ou cofre) ou ser separada em um signer dedicado, mantendo o cliente leve e sem material sensível.

Finalmente, a evolução natural é: o bot em Go continua decidindo quando agir, persiste o estado no SQLite, e a execução do swap passa a ser um contrato gRPC bem definido com protobufs para cotação e execução, entregando robustez operacional, isolamento de segredos e padronização de erros/logs para produção.


Referências

Go. (2025, 25 fevereiro). The Go programming language specification. go.dev. https://go.dev/ref/spec (Go)

Go. (n.d.). Documentation. go.dev. https://go.dev/doc/ (Go)

gRPC. (2024, 25 novembro). Basics tutorial | Go. grpc.io. https://grpc.io/docs/languages/go/basics/ (gRPC)

gRPC. (2024, 25 novembro). Quick start | Go. grpc.io. https://grpc.io/docs/languages/go/quickstart/ (gRPC)

Protocol Buffers. (n.d.). Protocol buffer basics: Go. protobuf.dev. https://protobuf.dev/getting-started/gotutorial/ (protobuf.dev)

Equipe go-ethereum (Geth). (2024, 24 maio). Getting started with Geth. geth.ethereum.org. https://geth.ethereum.org/docs/getting-started (go-ethereum)

Go Ethereum. (n.d.). github.com/ethereum/go-ethereum (API em Go). pkg.go.dev. https://pkg.go.dev/github.com/ethereum/go-ethereum (Go Packages)

modernc.org. (2025, 11 agosto). sqlite package (driver SQLite sem CGO). pkg.go.dev. https://pkg.go.dev/modernc.org/sqlite (Go Packages)

Joho. (n.d.). joho/godotenv [Repositório]. GitHub. https://github.com/joho/godotenv (GitHub)

Sushi. (2025, 13 setembro). Pricing API documentation. docs.sushi.com. https://docs.sushi.com/api/examples/pricing (docs.sushi.com)

gRPC-Go. (n.d.). google.golang.org/grpc (pacote em Go). pkg.go.dev. https://pkg.go.dev/google.golang.org/grpc (Go Packages)

💡Curtiu?

Se quiser trocar ideia sobre IA, cloud e arquitetura, me segue nas redes:

Publico conteúdos técnicos direto do campo de batalha. E quando descubro uma ferramenta que economiza tempo e resolve bem, como essa, você fica sabendo também.

Top comments (0)