# What is NextBlock?

Welcome to the NextBlock - Solana's Leading Transaction Sending Service.&#x20;

With the largest SWQoS stake pool in Solana and the most experienced team in transaction sending internals, we ensure you land fast so you can focus on what matters.


# Pricing & Rate Limits

Simple, scalable pricing. Upgrade/Downgrade freely.

##

<table data-full-width="true"><thead><tr><th width="191">Trial</th><th width="183">Entry</th><th width="218">Intermediate</th><th width="225">Advanced</th><th>Enterprise</th></tr></thead><tbody><tr><td>Free</td><td>$249/month</td><td>$749/month</td><td>$1,749/month</td><td>Custom Pricing - Contact Us</td></tr><tr><td>1 TX per 10s</td><td>5 TPS</td><td>20 TPS</td><td>50 TPS</td><td>100+ TPS</td></tr><tr><td>Dedicated SWQoS</td><td>Dedicated SWQoS</td><td>Dedicated SWQoS</td><td>Dedicated SWQoS</td><td>Dedicated SWQoS</td></tr><tr><td>1 Global API Key</td><td>1 Global API Key</td><td>1 Global API Key</td><td>1 Global API Key</td><td>1 Global API Key</td></tr><tr><td>IP, CIDR, HOST Access Controls</td><td>IP, CIDR, HOST Access Controls</td><td>IP, CIDR, HOST Access Controls</td><td>IP, CIDR, HOST Access Controls</td><td>IP, CIDR, HOST Access Controls</td></tr><tr><td>No Support</td><td>Limited Support</td><td>Discord Tickets Support</td><td>Direct Message Support</td><td>Direct Message Support</td></tr></tbody></table>


# Quickstart

### Endpoints

Choose the endpoint closest to your location:

* **Frankfurt**: frankfurt.nextblock.io
* **Amsterdam**: amsterdam.nextblock.io
* **London**: london.nextblock.io
* **Singapore**: singapore.nextblock.io
* **Tokyo**: tokyo.nextblock.io
* **New York**: ny.nextblock.io
* **Salt Lake City**: slc.nextblock.io
* **Dublin**: dublin.nextblock.io
* **Vilnius**: vilnius.nextblock.io

### Tip Wallets

Your TX must include a plain SOL Transfer IX to one of our tip wallets.

Higher tips = higher priority. Use the tip floor API to determine optimal tip amounts based on current network conditions.

* NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE
* NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2
* NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X
* NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb
* neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At
* nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG
* NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid
* nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc

### gRPC

If you want to use gRPC, start with the proto definitions in [nextblock-proto](https://github.com/nextblock-ag/nextblock-proto/) and the examples in this docs section.

### Anti-MEV

To enable Anti-MEV, set `frontRunningProtection: true` in your request as shown:

`curl -X 'POST'`\
`'https://frankfurt.nextblock.io/api/v2/submit'`\
`-header "Authorization: $AUTH_HEADER"`\
`-d '{ "transaction": {"content": "FtQ+KrJeNuiismV1Ke...DBABRQ=="}, "frontRunningProtection": true }'`

### Priority Fees & Retries

If anti-MEV is enabled, priority fee does not matter, only the NextBlock Tip matters.

If anti-MEV is disabled, ensure you use a good priority fee to maintain a competitive position amongst others during block production. Use the tip floor API to determine appropriate priority fees based on current network conditions.

When sending without `frontRunningProtection: true`, your transaction is sent via all channels: our internal validator network, Jito Bundles, and as a normal transaction to upcoming leaders.

NextBlock's intelligent retry system actively tracks on-chain activity levels and adjusts dynamically to send transactions more aggressively during congestion and less aggressively during downtime. Simply send us the transaction once - we take care of the rest, no retries needed on your end.


# GoLang Sample Code

```go
package main

import (
    "bytes"
    "context"
    "encoding/json"
    "github.com/gagliardetto/solana-go"
    "github.com/gagliardetto/solana-go/programs/system"
    "github.com/gagliardetto/solana-go/rpc"
    "io"
    "net/http"
    "testing"
)

type Payload struct {
    Transaction TransactionMessage `json:"transaction"`
}

type TransactionMessage struct {
    Content string `json:"content"`
}

func TestSendTx(T *testing.T) {
    cli := http.Client{}
    privateKey := solana.MustPrivateKeyFromBase58("")
    rpcClient := rpc.New("")
    bh, _ := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
    txBuilder := solana.NewTransactionBuilder()
    txBuilder.SetRecentBlockHash(bh.Value.Blockhash)
    txBuilder.AddInstruction(system.NewTransferInstruction(1000000, privateKey.PublicKey(), solana.MustPublicKeyFromBase58("nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc")).Build())
    tx, _ := txBuilder.Build()
    tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
        return &privateKey
    })
    tx64Str := tx.MustToBase64()
    txMsg := TransactionMessage{
        Content: tx64Str,
    }
    p := Payload{
        Transaction: txMsg,
    }
    t, _ := json.Marshal(p)

    req, err := http.NewRequest("POST", "https://frankfurt.nextblock.io/api/v2/submit", bytes.NewBuffer(t))
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("authorization", "")
    if err != nil {
        T.Error(err)
        return
    }
    do, err := cli.Do(req)
    if err != nil {
        T.Error(err)
        return
    }
    defer do.Body.Close()
    b, _ := io.ReadAll(do.Body)
    T.Log(string(b))
    if do.StatusCode == 200 {
        T.Log("Sent transaction with signature ", tx.Signatures[0].String())
    }
}
```

### CURL Example

`curl -XPOST -H 'authorization: api key' -H 'Content-Type: application/json' 'https://frankfurt.nextblock.io/api/v2/submit' -d '{ "transaction": {"content": "AjF+B...BCQ=="} }'`


# Bundle Sending Sample

```go
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"github.com/gagliardetto/solana-go"
	"github.com/gagliardetto/solana-go/programs/system"
	"github.com/gagliardetto/solana-go/rpc"
	"io"
	"net/http"
	"testing"
)

type Payload struct {
	Transaction TransactionMessage `json:"transaction"`
}

type TransactionMessage struct {
	Content string `json:"content"`
}

func TestSendTx(T *testing.T) {
	cli := http.Client{}
	privateKey := solana.MustPrivateKeyFromBase58("")
	rpcClient := rpc.New("")
	bh, _ := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
	txBuilder := solana.NewTransactionBuilder()
	txBuilder.SetRecentBlockHash(bh.Value.Blockhash)
	txBuilder.AddInstruction(system.NewTransferInstruction(1000001, privateKey.PublicKey(), solana.MustPublicKeyFromBase58("nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG")).Build())

	tx, _ := txBuilder.Build()
	tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
		return &privateKey
	})
	tx64Str := tx.MustToBase64()

	txMsg := TransactionMessage{
		Content: tx64Str,
	}
	entry := Entry{
		Transaction: txMsg,
	}

	p := BatchPayload{
		Transactions: []Entry{entry},
	}

	fmt.Println("Sending transaction", tx.Signatures[0].String())

	txBuilder = solana.NewTransactionBuilder()
	txBuilder.SetRecentBlockHash(bh.Value.Blockhash)
	txBuilder.AddInstruction(system.NewTransferInstruction(1000002, privateKey.PublicKey(), solana.MustPublicKeyFromBase58("nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG")).Build())

	tx, _ = txBuilder.Build()
	tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
		return &privateKey
	})
	tx64Str = tx.MustToBase64()

	txMsg = TransactionMessage{
		Content: tx64Str,
	}
	entry = Entry{
		Transaction: txMsg,
	}

	p.Transactions = append(p.Transactions, entry)

	t, _ := json.Marshal(p)

	fmt.Println(string(t))
	fmt.Println(privateKey.PublicKey().String())

	req, err := http.NewRequest("POST", "https://ny.nextblock.io/api/v2/submit-batch", bytes.NewBuffer(t))
	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("authorization", "advanced123456789-ABCD%2FyfHouQJyiWObY1ntyFSBI3G3NNCczaWKRJPvc%3D")
	if err != nil {
		T.Error(err)
		return
	}
	do, err := cli.Do(req)
	if err != nil {
		T.Error(err)
		return
	}
	defer do.Body.Close()
	b, _ := io.ReadAll(do.Body)
	T.Log(string(b))
	if do.StatusCode == 200 {
		T.Log("Sent transaction with signature ", tx.Signatures[0].String())
	} else {
		fmt.Println(do.StatusCode)
		T.Error("Failed to send transaction")
	}
}

```

### CURL Example

`curl -XPOST -H 'authorization: api key' -H 'Content-Type: application/json' 'https://frankfurt.nextblock.io/api/v2/submit-batch' -d '{`"entries": \[ { "transaction": { "content": "ASrTNkPOT...." } } ] }`'`


# Contact

For all inquiries, please contact us via one of the following channels:

* Email: <contact@nextblock.io>
* Discord: [https://discord.gg/nextblock](https://discord.gg/nextblocksol)


# Basics

## Choose an Interface

NextBlock exposes multiple interfaces depending on what you are building:

* **HTTP**: easiest to integrate from any language using REST requests
* **gRPC**: recommended general-purpose interface if you want the full submission feature set
* **QUIC**: lowest-latency submission path for advanced traders sending raw signed transaction bytes
* **TX Stream**: separate real-time stream of transactions for monitoring and trading strategies

Use **gRPC** or **HTTP** if you need submission options such as front-running protection, revert-on-fail, disable-retries, or bundle submission.

Use **QUIC** if you want the smallest possible transport overhead and only need to send raw signed transaction bytes. QUIC does **not** support the extra gRPC submission flags or the batch submission endpoint.

## gRPC

We recommend gRPC for most direct integrations with the main NextBlock API. The proto definitions are available in [`nextblock-proto`](https://github.com/nextblock-ag/nextblock-proto).

## Tip Floor API

The tip floor API returns landed tip percentiles from the last 5 minutes. You can use it to adjust your tip amounts based on current network conditions.

All tip floor values are returned in **SOL**, not lamports.

```json
{
  "time": "2025-05-13T10:41:45Z",
  "landed_tips_25th_percentile": 0.0011,
  "landed_tips_50th_percentile": 0.005000001,
  "landed_tips_75th_percentile": 0.01555,
  "landed_tips_95th_percentile": 0.09339195639999975,
  "landed_tips_99th_percentile": 0.4846427910400001,
  "ema_landed_tips_50th_percentile": 0.005989477267191758
}
```


# Authentication

## Main API: HTTP, gRPC, and QUIC

The main NextBlock API uses an API key.

* For **HTTP**, send the API key in the `Authorization` header.
* For **gRPC**, send the API key in the `authorization` metadata field.
* For **QUIC**, send the API key once during the initial auth handshake on the bidirectional auth stream.

```
authorization: your-api-key-here
```

## TX Stream Uses Different Authentication

The TX Stream service does **not** use the same API-key authentication flow as the main API.

TX Stream access is tied to the Solana wallet that paid for the service, and the client authenticates by signing an auth message with that wallet.

See [TX Stream API](/api/tx-stream) for the TX Stream-specific authentication flow.


# Submit Transaction

### POST /api/v2/submit

#### Description

Submits a single signed transaction to NextBlock.

### Request

* **URL: `/api/v2/submit`**
* **Method:** `POST`
* **Headers:**
  * `Content-Type: application/json`
  * `Authorization: <token>`

**Request Body**

```json
{
  "transaction": {
    "content": "base64-encoded-signed-transaction"
  },
  "skipPreFlight": true,
  "frontRunningProtection": false,
  "disableRetries": false,
  "revertOnFail": false,
  "snipeTransaction": false
}
```

### Notes

* `transaction.content` must be the base64-encoded bytes of a fully signed Solana transaction.
* The optional flags above are available on the HTTP and gRPC submission paths.
* If you use [QUIC Transaction Submission](/api/quic-transaction-submission), these flags are **not** available. QUIC accepts only the raw signed transaction bytes.

### **Response**

**Success (200 OK)**

```json
{
  "signature": "tx-signature",
  "uuid": "jito-bundle-uuid"
}
```

**Error (400 Bad Request)**

```json
{
  "code": 2,
  "message": "fee too low; transaction contains low tip",
  "details": []
}
```


# Submit Batched Transactions

### POST /api/v2/submit-batch

#### Description

Submits an atomic bundle of 2-4 signed transactions to NextBlock.

### Request

* **URL: `/api/v2/submit-batch`**
* **Method:** `POST`
* **Headers:**
  * `Content-Type: application/json`
  * `Authorization: <token>`

**Request Body**

```json
{
    "entries": [
        {
            "transaction": {
                "content": "ASrTNkPOTud8TsxOKcumvY7supstxpU204Md03mm7XPZw2Y/zm0wvINTRxEcsI1HmPOYFJ/IDz6OCq7fSqM/LQUBAAEDgIwXPqhLVB/6ZadyORj58IB7OZZgqVvqNrNxdQF2AuILlm3gHQjNYA7I5Y9y2P3umSGL05F3f0ycmaLEwKYbVwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy7mAVCwl2qlxV1malhPHTmIf/Ndy3WDQQb3uvo/2TlYBAgIAAQwCAAAAQUIPAAAAAAA="
            }
        },
        {
            "transaction": {
                "content": "AVFRplUyysWi0v21B6NVE9t5hMEdDIh1QZNBONweQ/zFgf40D75lazBWAjVyqVI2hNdYpXDJzfP7tMfHav5w2AYBAAEDgIwXPqhLVB/6ZadyORj58IB7OZZgqVvqNrNxdQF2AuILlm3gHQjNYA7I5Y9y2P3umSGL05F3f0ycmaLEwKYbVwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy7mAVCwl2qlxV1malhPHTmIf/Ndy3WDQQb3uvo/2TlYBAgIAAQwCAAAAQkIPAAAAAAA="
            }
        }
    ]
}
```

### **Response**

**Success (200 OK)**

```json
{
  "signature": "bundle-signature"
}
```

**Error (400 Bad Request)**

```json
{
  "code": 2,
  "message": "fee too low; transaction contains low tip",
  "details": []
}
```

### Notes

* Each `entries[].transaction.content` value must contain a base64-encoded signed transaction.
* Bundles must contain between `2` and `4` transactions.
* This endpoint is available on the HTTP and gRPC paths.
* QUIC does **not** support atomic bundle submission. QUIC sends one raw signed transaction per stream.


# QUIC Transaction Submission

NextBlock supports low-latency transaction submission over raw QUIC. Instead of sending base64 inside gRPC, you keep one QUIC connection open and write the signed transaction bytes directly to a unidirectional stream.

This path is designed for advanced traders who already have signed Solana transaction bytes and want the lowest-latency submission path with the smallest possible transport overhead.

## When To Use QUIC

Use QUIC if:

* you already have fully signed Solana transaction bytes
* you want the lowest possible submission latency
* you do not need the extra submission options available on HTTP or gRPC

Use HTTP or gRPC instead if you need:

* front-running protection
* revert-on-fail
* disable-retries
* snipe mode
* atomic bundle submission

## Available Endpoints

Choose the region closest to your bot or signer. All QUIC endpoints listen on port `11100`.

* Frankfurt: `frankfurt.nextblock.io:11100`
* New York: `ny.nextblock.io:11100`
* Vilnius: `vilnius.nextblock.io:11100`
* Tokyo: `tokyo.nextblock.io:11100`
* Singapore: `singapore.nextblock.io:11100`
* Dublin: `dublin.nextblock.io:11100`
* London: `london.nextblock.io:11100`
* Salt Lake City: `slc.nextblock.io:11100`
* Amsterdam: `amsterdam.nextblock.io:11100`

## How It Works

1. Open a QUIC connection to a regional endpoint with ALPN `nb-tx/1`.
2. Open one bidirectional stream and send your API key as raw UTF-8 bytes.
3. Close your write side, then read a single-byte auth response.
4. If the response is `0x00`, the connection is authenticated and ready.
5. For each transaction, open a new unidirectional stream and write the raw serialized transaction bytes.

## Important Notes

* Transactions must fit within Solana's normal transaction size limit of `1232` bytes.
* Submission is fire-and-forget. After the bytes are written, there is no per-transaction response stream.
* QUIC submission does not support the extra gRPC submission flags such as front-running protection, revert-on-fail, disable-retries, or snipe mode. You only send the raw signed transaction bytes.
* QUIC also does not support the atomic batch submission endpoint.
* Invalid or rate-limited transactions may be dropped server-side without a response.
* Keep one connection open and reuse it for many transactions instead of reconnecting for every send.

## Language Examples

* [Rust QUIC example](/api/examples/rust/quic)
* [Go QUIC example](/api/examples/golang/quic)
* [Python QUIC example](/api/examples/python/quic)
* [TypeScript QUIC example](/api/examples/javascript/quic)


# TX Stream API

The NextBlock TX Stream API provides real-time streaming of Solana transactions filtered by program IDs. This independent service allows trading bots to subscribe to transaction streams and receive fast notifications for transactions involving specific programs, enabling quick detection of new DEX launches and trading opportunities.

To be authenticated to use **NextStream**, you have to send SOL to nextstream.sol. 5 SOL = 31 days of access. You can also send less to test the service for a few days. The minimum amount is 1 day (0.17 SOL). Upon sending SOL to nextstream.sol, the address from which you sent the SOL becomes authenticated for one concurrent connection per region. You **have to use the SOL sender wallet to authenticate** to the NextStream server.

Examples in Rust, TypeScript and Golang are available at [github.com/nextblock-ag/nextblock-stream-examples](https://github.com/nextblock-ag/nextblock-stream-examples)

## Overview

To subscribe to NextStream:

1. **Build an authentication message**: `{domain}|{publickey}|{nonce}|{timestamp}`

* `domain`: the host you are connecting to (e.g. `fra.stream.nextblock.io:22221`)
* `publickey`: the Solana public key of the account that sent the fee transaction
* `nonce`: random integer
* `timestamp`: current unix time (seconds)

2. **Sign the message** with the corresponding Solana private key.
3. **Send a subscription request** (`NextStreamSubscription`) with:

* `AuthenticationPublickey`
* `AuthenticationMessage`
* `AuthenticationSignature` (base58)
* List of `accounts` you want to filter for

4. **Receive streamed messages**, each containing a `Packet` with:

* `transaction` (binary-encoded Solana transaction)
* `slot`

## Proto Specification

```protobuf
syntax = "proto3";

package stream;

option go_package = "nextblock-stream-examples/golang-example/protos";

message NextPacket {
    bytes transaction = 1;                  // raw transaction bytes
    uint64 slot = 2;
}

message NextStreamSubscription {
    string authentication_publickey = 1;    // the publickey you used to send 5 SOL to nextstream.sol
    string authentication_message = 2;      // the finished authentication_message:
    string authentication_signature = 3;    // base58 signature, signed_message signed by authenticated_publieky

    repeated string accounts = 4;           // base58 accounts to subscribe to
}

message NextStreamNotification {
    NextPacket packet = 1;
}

service NextStreamService {
    rpc SubscribeNextStream (NextStreamSubscription) returns (stream NextStreamNotification) {}
}
```

## Available Endpoints

Choose the endpoint closest to your location for optimal latency:

* **Frankfurt**: `fra.stream.nextblock.io:22221`
* **Amsterdam**: `amsterdam.stream.nextblock.io:22221`
* **London**: `london.stream.nextblock.io:22221`
* **Singapore**: `singapore.stream.nextblock.io:22221`
* **Tokyo**: `tokyo.stream.nextblock.io:22221`
* **New York**: `ny.stream.nextblock.io:22221`
* **Salt Lake City**: `slc.stream.nextblock.io:22221`
* **Dublin**: `dublin.stream.nextblock.io:22221`
* **Vilnius**: `vilnius.stream.nextblock.io:22221`

## Examples

* [Go Example](/api/tx-stream/golang) - Complete Go implementation with connection setup
* [Rust Example](/api/tx-stream/rust) - Rust implementation using Tonic
* [TypeScript Example](/api/tx-stream/typescript) - TypeScript implementation for Node.js
* [HTTP Example](/api/tx-stream/http) - HTTP alternatives

## Use Cases

1. **Trading Bots**: Real-time transaction detection for automated trading strategies and sniping new DEX launches

## Best Practices

1. **Choose optimal endpoint**: Use the endpoint closest to your location
2. **Filter efficiently**: Subscribe only to programs you need to reduce bandwidth
3. **Handle reconnections**: Implement automatic reconnection logic
4. **Process quickly**: Handle incoming transactions efficiently to avoid backlog
5. **Use connection pooling**: For high-throughput applications
6. **Monitor connection health**: Implement heartbeat/keepalive mechanisms


# Go Example

Complete Go implementation for streaming Solana transactions from NextBlock's TX Stream API, optimized for trading bots and fast transaction detection.

The example is hosted at [github.com/nextblock-ag/nextblock-stream-examples](https://github.com/nextblock-ag/nextblock-stream-examples/tree/main/golang-example)

## Prerequisites

```bash
go mod init nextblock-tx-stream-example
go get github.com/gagliardetto/binary github.com/gagliardetto/solana-go
go get google.golang.org/grpc

# Generate the TX Stream proto client from the proto specification
# In the example repo, dependencies and the generated .go files are already available
```

## Example

```go
package main

import (
	"context"
	"fmt"
	"log"
	"math"
	"math/rand/v2"
	"time"

	bin "github.com/gagliardetto/binary"
	"github.com/gagliardetto/solana-go"
	"github.com/nextblock-stream-examples/golang-example/protos"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/keepalive"
)

/*
the auth message needs to be built clients-side.
it is a pipe-separated string made of:
1. domain that's being connected to (e.g. fra.stream.nextblock.io)
2. the publickey that sent the fee to strmuYvHKeA1qvHqooUpwUk2BFwaAmMbK9WXY9mh2GJ
3. a random nonce
4. the current unix timestamp

it is then signed by the supplied publickey.
*/

func buildAuthMessage(domain string, authenticatedPublicKey solana.PublicKey) string {
	return fmt.Sprintf("%s|%s|%d|%d", domain, authenticatedPublicKey.String(), rand.IntN(math.MaxInt64), time.Now().Unix())
}

func main() {
	ctx := context.Background()

	domain := "ny.stream.nextblock.io:22221"
	authenticationPrivateKey := solana.MustPrivateKeyFromBase58("YOUR_PRIVATE_KEY")
	authenticationPublicKey := authenticationPrivateKey.PublicKey()

	authenticationMessage := buildAuthMessage(domain, authenticationPrivateKey.PublicKey())
	authenticationSignature, err := authenticationPrivateKey.Sign([]byte(authenticationMessage))
	if err != nil {
		log.Fatalf("error signing authMessage: %v", err)
	}

	creds := insecure.NewCredentials()
	conn, err := grpc.NewClient(
		domain,
		grpc.WithTransportCredentials(creds),
		grpc.WithKeepaliveParams(keepalive.ClientParameters{
			Time:                5 * time.Second,
			Timeout:             time.Second,
			PermitWithoutStream: false,
		}),
	)
	if err != nil {
		log.Fatalf("error creating new grpc conn: %v", err)
	}

	client := protos.NewNextStreamServiceClient(conn)

	sub, err := client.SubscribeNextStream(
		ctx,
		&protos.NextStreamSubscription{
			AuthenticationPublickey: authenticationPublicKey.String(),
			AuthenticationMessage:   authenticationMessage,
			AuthenticationSignature: authenticationSignature.String(),
			Accounts:                []string{solana.MustPublicKeyFromBase58("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8").String()},
		},
	)
	if err != nil {
		log.Fatalf("error SubscribeNextStream: %v", err)
	}

	for {
		msg, err := sub.Recv()
		if err != nil {
			log.Fatalf("error receiving from sub: %v", err)
		}

		tx := new(solana.Transaction)
		err = tx.UnmarshalWithDecoder(bin.NewBinDecoder(msg.Packet.Transaction))
		if err != nil {
			log.Fatalf("error unmarshaslling tx: %v", err)
		}

		log.Printf("got new sig %s on slot %d", tx.Signatures[0], msg.Packet.Slot)
	}
}
```

## Usage Examples

```bash
# Log signature + slot for all filtered transactions
go run main.go
```

## Available Endpoints

Choose the endpoint closest to your location:

* **Frankfurt**: `fra.stream.nextblock.io:22221`
* **Amsterdam**: `amsterdam.stream.nextblock.io:22221`
* **London**: `london.stream.nextblock.io:22221`
* **Singapore**: `singapore.stream.nextblock.io:22221`
* **Tokyo**: `tokyo.stream.nextblock.io:22221`
* **New York**: `ny.stream.nextblock.io:22221`
* **Salt Lake City**: `slc.stream.nextblock.io:22221`
* **Dublin**: `dublin.stream.nextblock.io:22221`
* **Vilnius**: `vilnius.stream.nextblock.io:22221`

## Best Practices for Trading Bots

1. **Match the endpoint requirements**: Follow the transport settings shown in the example for the current TX Stream service
2. **Filter efficiently**: Only subscribe to programs you need to reduce bandwidth
3. **Handle reconnections**: Implement automatic reconnection logic
4. **Process quickly**: Avoid blocking the stream receiver
5. **Monitor performance**: Track processing latency and throughput
6. **Implement graceful shutdown**: Handle interrupt signals properly
7. **Use multiple endpoints**: Implement redundancy for critical applications


# Rust Example

Complete Rust implementation for streaming Solana transactions from NextBlock's TX Stream API using Tonic.

The example is hosted at [github.com/nextblock-ag/nextblock-stream-examples](https://github.com/nextblock-ag/nextblock-stream-examples/tree/main/rust-example)

## Prerequisites

Add these dependencies to your `Cargo.toml`:

```toml
[package]
name = "rust-example"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.99"
bincode = "1.3.3"
bs58 = "0.5"
prost = "0.13"
prost-types = "0.13"
protobuf = "3.7.2"
rand = "0.8"
solana-sdk = "3.0.0"
time = "0.3"
tokio = { version = "1.39", features = ["rt-multi-thread", "macros"] }
tonic = { version = "0.12", features = ["gzip"] }

[build-dependencies]
tonic-build = "0.12"
```

## Proto build script build.rs

```rust
use std::{env, path::PathBuf};

fn main() {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());

    let repo_root = manifest_dir
        .parent()
        .expect("rust-example should have a parent directory")
        .to_path_buf();

    let proto = repo_root.join("stream.proto");
    if !proto.exists() {
        panic!("Proto not found at {}", proto.display());
    }

    let out_dir = manifest_dir.join("src/protos");

    println!("cargo:rerun-if-changed={}", proto.display());
    println!("cargo:rerun-if-changed={}", repo_root.display());

    std::fs::create_dir_all(&out_dir).expect("create src/protos");

    tonic_build::configure()
        .build_client(true)
        .build_server(false)
        .out_dir(out_dir)
        .compile_protos(
            &[proto.to_string_lossy().to_string()],
            &[repo_root.to_string_lossy().to_string()],
        )
        .expect("Failed to compile protos");
}
```

## Example

```rust
use std::{
    str::FromStr,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

use rand::Rng;
use solana_sdk::{
    pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::VersionedTransaction,
};

use anyhow::{Context, Result, bail};
use tonic::{
    Request,
    transport::{Channel, Endpoint},
};

use crate::stream::{NextStreamSubscription, next_stream_service_client::NextStreamServiceClient};

#[path = "protos/stream.rs"]
pub mod stream;

/*
the auth message needs to be built clients-side.
it is a pipe-separated string made of:
1. domain that's being connected to (e.g. fra.stream.nextblock.io)
2. the publickey that sent the fee to strmuYvHKeA1qvHqooUpwUk2BFwaAmMbK9WXY9mh2GJ
3. a random nonce
4. the current unix timestamp

it is then signed by the supplied publickey.
*/

fn build_auth_message(domain: &str, pubkey: &Pubkey) -> String {
    let nonce: u64 = rand::thread_rng().r#gen();
    let ts = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    format!("{}|{}|{}|{}", domain, pubkey.to_string(), nonce, ts)
}

async fn make_insecure_channel(domain: &str) -> Result<Channel> {
    let ep = Endpoint::from_shared(format!("http://{}", domain))?
        .http2_keep_alive_interval(Duration::from_secs(5))
        .keep_alive_while_idle(false);
    Ok(ep.connect().await?)
}

#[tokio::main]
async fn main() -> Result<()> {
    let domain = "fra.stream.nextblock.io:22221";
    let private_key_b58 = "YOUR_PRIVATE_KEY";
    if private_key_b58.is_empty() {
        bail!("Set `private_key_b58` to your base58-encoded Solana private key.");
    }

    let authentication_keypair = Keypair::from_base58_string(private_key_b58);
    let authentication_pubkey = authentication_keypair.pubkey();

    let authentication_message = build_auth_message(domain, &authentication_pubkey);
    let authentication_signature =
        authentication_keypair.sign_message(authentication_message.as_bytes());

    let channel = make_insecure_channel(domain).await?;
    let mut client = NextStreamServiceClient::new(channel);

    let req = NextStreamSubscription {
        authentication_publickey: authentication_pubkey.to_string(),
        authentication_message: authentication_message,
        authentication_signature: authentication_signature.to_string(),
        accounts: vec![
            Pubkey::from_str("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8")
                .unwrap()
                .to_string(),
        ],
    };

    let mut stream = client
        .subscribe_next_stream(Request::new(req))
        .await?
        .into_inner();

    while let Some(msg) = stream.message().await.context("recv")? {
        if let Some(packet) = msg.packet {
            let tx: VersionedTransaction = bincode::deserialize(&packet.transaction)?;
            let first_sig = tx
                .signatures
                .get(0)
                .map(|s| s.to_string())
                .unwrap_or_else(|| "<no signatures>".to_string());
            println!("got new sig {} on slot {}", first_sig, packet.slot);
        }
    }
    Ok(())
}
```

## Usage Examples

```bash
# Log signature + slot for all filtered transactions
cargo run
```

## Available Endpoints

Choose the endpoint closest to your location:

* **Frankfurt**: `fra.stream.nextblock.io:22221`
* **Amsterdam**: `amsterdam.stream.nextblock.io:22221`
* **London**: `london.stream.nextblock.io:22221`
* **Singapore**: `singapore.stream.nextblock.io:22221`
* **Tokyo**: `tokyo.stream.nextblock.io:22221`
* **New York**: `ny.stream.nextblock.io:22221`
* **Salt Lake City**: `slc.stream.nextblock.io:22221`
* **Dublin**: `dublin.stream.nextblock.io:22221`
* **Vilnius**: `vilnius.stream.nextblock.io:22221`

## Best Practices

1. **Match the endpoint requirements**: Follow the transport settings shown in the example for the current TX Stream service
2. **Filter efficiently**: Only subscribe to programs you need to reduce bandwidth
3. **Handle reconnections**: Implement automatic reconnection logic
4. **Process quickly**: Avoid blocking the stream receiver
5. **Monitor performance**: Track processing latency and throughput
6. **Implement graceful shutdown**: Handle interrupt signals properly
7. **Use multiple endpoints**: Implement redundancy for critical applications


# TypeScript Example

Complete JavaScript/TypeScript implementation for streaming Solana transactions from NextBlock's TX Stream API, optimized for trading bots and fast transaction detection.

The example is hosted at [github.com/nextblock-ag/nextblock-stream-examples](https://github.com/nextblock-ag/nextblock-stream-examples/tree/main/typescript-example)

## Prerequisites

```bash
bun add @grpc/grpc-js @grpc/proto-loader
bun add @solana/addresses @solana/codec-strings @solana/kit

# Generate the TX Stream proto client from the proto specification
# In the example repo, dependencies and the generated .ts files are already available
```

## Example

```typescript
import * as grpc from "@grpc/grpc-js";

import {
    NextStreamNotification,
    NextStreamServiceClient,
} from "./protos/stream";
import { NextStreamSubscription } from "./protos/stream";
import {
    createKeyPairFromBytes,
    getAddressFromPublicKey,
    getBase58Decoder,
    getBase58Encoder,
    getCompiledTransactionMessageDecoder,
    getSignatureFromTransaction,
    getTransactionDecoder,
    getUtf8Encoder,
    signBytes,
    type Address,
} from "@solana/kit";

/*
the auth message needs to be built clients-side.
it is a pipe-separated string made of:
1. domain that's being connected to (e.g. fra.stream.nextblock.io)
2. the publickey that sent the fee to strmuYvHKeA1qvHqooUpwUk2BFwaAmMbK9WXY9mh2GJ
3. a random nonce
4. the current unix timestamp

it is then signed by the supplied publickey.
*/

function buildAuthMessage(
    domain: string,
    authenticatedPublicKey: Address
): string {
    const nonce = Math.random().toString();
    const ts = Math.floor(Date.now());
    return `${domain}|${authenticatedPublicKey.toString()}|${nonce}|${ts}`;
}

async function main() {
    const domain = "fra.stream.nextblock.io:22221";
    const authenticationPrivateKey = await createKeyPairFromBytes(
        getBase58Encoder().encode("YOUR_PRIVATE_KEY")
    );
    const authenticationPublicKey = await getAddressFromPublicKey(
        authenticationPrivateKey.publicKey
    );

    const authenticationMessage = buildAuthMessage(
        domain,
        authenticationPublicKey
    );
    const authenticationSignature = await signBytes(
        authenticationPrivateKey.privateKey,
        getUtf8Encoder().encode(authenticationMessage)
    );

    const client = new NextStreamServiceClient(
        domain,
        grpc.credentials.createInsecure(),
        {
            "grpc.keepalive_time_ms": 5000,
            "grpc.keepalive_timeout_ms": 1000,
            "grpc.keepalive_permit_without_calls": 0,
        }
    );

    const req: NextStreamSubscription = {
        authenticationPublickey: authenticationPublicKey.toString(),
        authenticationMessage,
        authenticationSignature: getBase58Decoder().decode(
            authenticationSignature
        ),
        accounts: ["675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"],
    };

    const stream = client.subscribeNextStream(req);

    stream.on("data", (msg: NextStreamNotification) => {
        try {
            const packet = msg.packet;
            if (!packet) {
                return;
            }

            const txBytes = packet.transaction;
            const slot = packet.slot;

            const tx = getTransactionDecoder().decode(txBytes);
            const message = getCompiledTransactionMessageDecoder().decode(
                tx.messageBytes
            );

            console.log(
                `got new sig ${getSignatureFromTransaction(
                    tx
                ).toString()} on slot ${slot}`
            );
        } catch (e) {
            console.error("error processing message:", e);
        }
    });

    stream.on("error", (err) => {
        console.error("error receiving from sub:", err);
        process.exit(1);
    });

    stream.on("end", () => {
        console.error("stream ended");
        process.exit(0);
    });
}

main();
```

## Usage Examples

```bash
# Log signature + slot for all filtered transactions
bun index.ts
```

## Available Endpoints

Choose the endpoint closest to your location:

* **Frankfurt**: `fra.stream.nextblock.io:22221`
* **Amsterdam**: `amsterdam.stream.nextblock.io:22221`
* **London**: `london.stream.nextblock.io:22221`
* **Singapore**: `singapore.stream.nextblock.io:22221`
* **Tokyo**: `tokyo.stream.nextblock.io:22221`
* **New York**: `ny.stream.nextblock.io:22221`
* **Salt Lake City**: `slc.stream.nextblock.io:22221`
* **Dublin**: `dublin.stream.nextblock.io:22221`
* **Vilnius**: `vilnius.stream.nextblock.io:22221`

## Best Practices for Trading Bots

1. **Match the endpoint requirements**: Follow the transport settings shown in the example for the current TX Stream service
2. **Filter efficiently**: Only subscribe to programs you need to reduce bandwidth
3. **Handle reconnections**: Implement automatic reconnection logic
4. **Process quickly**: Avoid blocking the stream receiver
5. **Monitor performance**: Track processing latency and throughput
6. **Implement graceful shutdown**: Handle interrupt signals properly
7. **Use multiple endpoints**: Implement redundancy for critical applications


# HTTP Example

The TX Stream API is a streaming service and does not expose a direct HTTP equivalent.

If you need real-time transaction monitoring, use the TX Stream gRPC service.

If you need transaction submission, use the main NextBlock HTTP, gRPC, or QUIC APIs instead.

## Overview

Since the TX Stream API is gRPC-only, an HTTP-based trading bot integration usually looks like this:

1. **Use TX Stream gRPC** for real-time monitoring
2. **Use the main HTTP API** for transaction submission and tip floor monitoring

## Recommendation

For trading bot applications that need fast transaction detection for sniping new DEX launches, the gRPC TX Stream API is strongly recommended due to its real-time streaming capabilities.

### Use gRPC TX Stream API (Recommended)

For trading bot transaction monitoring, use the TX Stream gRPC API with one of these examples:

* [Go TX Stream Example](/api/tx-stream/golang) - Complete Go implementation
* [TypeScript TX Stream Example](/api/tx-stream/typescript) - Node.js implementation
* [Rust TX Stream Example](/api/tx-stream/rust) - Rust with Tonic

There is currently no Python TX Stream example in this docs section.

### HTTP API Integration

You can use the existing NextBlock HTTP API endpoints for transaction submission:

<pre class="language-bash"><code class="lang-bash"><strong># Submit transactions to NextBlock
</strong>curl -X POST https://frankfurt.nextblock.io/api/v2/submit \
  -H "Content-Type: application/json" \
  -H "Authorization: your-api-key-here" \
  -d '{
    "transaction": {
      "content": "base-64-encoded-transaction"
    }
  }'

<strong># Get tip floor data for optimization
</strong>curl -X GET https://frankfurt.nextblock.io/api/v2/tipfloor \
  -H "Authorization: your-api-key-here"
</code></pre>

## Available HTTP Endpoints

For transaction submission and tip floor data:

* **Frankfurt**: `https://frankfurt.nextblock.io`
* **Amsterdam**: `https://amsterdam.nextblock.io`
* **London**: `https://london.nextblock.io`
* **Singapore**: `https://singapore.nextblock.io`
* **Tokyo**: `https://tokyo.nextblock.io`
* **New York**: `https://ny.nextblock.io`
* **Salt Lake City**: `https://slc.nextblock.io`
* **Dublin**: `https://dublin.nextblock.io`
* **Vilnius**: `https://vilnius.nextblock.io`

## Best Practices

1. **Use TX Stream gRPC for real-time monitoring**: this is the actual streaming interface
2. **Use HTTP, gRPC, or QUIC for submission**: pick the main API transport that matches your needs
3. **Monitor tip floors**: use the tip floor endpoint to optimize transaction tips
4. **Choose the closest endpoint**: lower network latency generally improves performance
5. **Handle rate limits and retries**: implement appropriate client-side error handling


# Examples

Use the examples section based on the transport and level of control you need:

* **HTTP**: simplest way to integrate from any language using REST requests
* **gRPC**: recommended general-purpose integration if you want the full submission feature set
* **QUIC**: lowest-latency submission path for advanced traders sending raw signed transaction bytes
* **TX Stream**: separate real-time monitoring service for transaction streams, not a submission API

## Which Path Should You Choose?

* Start with **HTTP** if you want the quickest integration and are fine working with JSON requests.
* Use **gRPC** if you want language-native clients and support for submission options such as front-running protection, revert-on-fail, disable-retries, and bundle submission.
* Use **QUIC** if latency is your top priority and you only need to send raw signed transaction bytes. QUIC does **not** support the extra gRPC submission flags or the batch submission endpoint.
* Use **TX Stream** if you want to monitor live transactions for trading or analytics. It uses a different authentication model from the main API.

## Language Sections

* [Golang](/api/examples/golang)
* [Rust](https://github.com/nextblock-ag/nextblock-docs/blob/main/api/examples/rust/README.md)
* [Python](/api/examples/python)
* [JavaScript/TypeScript](/api/examples/javascript)
* [HTTP](/api/examples/http)


# Golang

Complete Go examples for integrating with NextBlock's gRPC API and QUIC transaction submission.

## Examples

* [Connection](/api/examples/golang/connection) - Establish gRPC connections with authentication
* [QUIC Transaction Submission](/api/examples/golang/quic) - Send raw signed transaction bytes over QUIC
* [Submit Single Transactions](/api/examples/golang/submit-single-transactions) - Send individual transactions with tips
* [Submit Batched Transactions](/api/examples/golang/submit-batched-transactions) - Send atomic transaction bundles
* [Tip Floor Stream](/api/examples/golang/tip-floor-stream) - Stream real-time tip floor data
* [Keepalive](/api/examples/golang/keepalive) - Maintain persistent connections


# Connection

Establish a connection to NextBlock's gRPC API from Go.

This page shows the connection pattern and API-key credentials wiring. Replace the generated client import and constructor with the client generated from [`nextblock-proto`](https://github.com/nextblock-ag/nextblock-proto).

## Prerequisites

```bash
go mod init nextblock-example
go get google.golang.org/grpc
go get github.com/gagliardetto/solana-go

# Clone the proto specs and generate the Go client
git clone https://github.com/nextblock-ag/nextblock-proto
# Follow the Go generation instructions in that repository
```

## Example

```go
package main

import (
	"context"
	"crypto/x509"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/keepalive"
)

type ApiKeyCredentials struct {
	apiKey     string
	requireTLS bool
}

func NewApiKeyCredentials(apiKey string, requireTLS bool) *ApiKeyCredentials {
	return &ApiKeyCredentials{
		apiKey:     apiKey,
		requireTLS: requireTLS,
	}
}

func (a *ApiKeyCredentials) RequireTransportSecurity() bool {
	return a.requireTLS
}

func (a *ApiKeyCredentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
	return map[string]string{
		"authorization": a.apiKey,
	}, nil
}

var keepAliveParams = keepalive.ClientParameters{
	Time:                time.Minute,
	Timeout:             15 * time.Second,
	PermitWithoutStream: true,
}

func ConnectToNextblock(address string, apiKey string, useTLS bool) (*grpc.ClientConn, error) {
	var opts []grpc.DialOption

	if useTLS {
		pool, err := x509.SystemCertPool()
		if err != nil {
			return nil, err
		}

		creds := credentials.NewClientTLSFromCert(pool, "")
		opts = append(opts, grpc.WithTransportCredentials(creds))
	} else {
		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	}

	opts = append(opts, grpc.WithKeepaliveParams(keepAliveParams))
	opts = append(opts, grpc.WithPerRPCCredentials(NewApiKeyCredentials(apiKey, useTLS)))

	return grpc.NewClient(address, opts...)
}
```

## Usage Example

```go
func main() {
	const FrankfurtEndpoint = "frankfurt.nextblock.io:443"

	apiKey := "<your-api-key-here>"

	// Prefer TLS by default.
	conn, err := ConnectToNextblock(FrankfurtEndpoint, apiKey, true)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// Replace this with your generated client type.
	// nextblockApiClient := api.NewApiClient(conn)

	// You can now use the generated client for API calls.
}
```

## Connection Best Practices

1. **Use TLS by default**: prefer `useTLS: true` unless you intentionally operate in a trusted internal environment
2. **Keep connections alive**: reuse a single gRPC connection for multiple requests
3. **Handle errors gracefully**: implement retry logic around transient network failures
4. **Close connections cleanly**: use `defer conn.Close()` when the process exits
5. **Choose the closest endpoint**: lower network latency usually improves performance

## Available Endpoints

* **Frankfurt**: `frankfurt.nextblock.io:443` (Europe)
* **Amsterdam**: `amsterdam.nextblock.io:443` (Europe)
* **London**: `london.nextblock.io:443` (Europe)
* **Singapore**: `singapore.nextblock.io:443` (Asia)
* **Tokyo**: `tokyo.nextblock.io:443` (Asia)
* **New York**: `ny.nextblock.io:443` (US East)
* **Salt Lake City**: `slc.nextblock.io:443` (US West)
* **Dublin**: `dublin.nextblock.io:443` (Europe)
* **Vilnius**: `vilnius.nextblock.io:443` (Europe)


# Keepalive

Keeping connections alive allows for faster deliveries, especially when using secured connections. \
In order to send a keep alive ping, once you have initialised your api client as described in the previous section you can start an asynchronous task, which sends a keep alive ping every minute. There is no real need to consume the response as it's simply an empty pong response.&#x20;

```go
go func() {
  for {
    _, _ = nextblockApiClient.Ping(context.Background())
    time.Sleep(time.Minute)
  }
}()
```


# Tip floor stream

Stream real-time tip floor data from NextBlock to optimize your transaction tips dynamically. This example shows a complete implementation including connection setup, authentication, and tip calculation.

## Prerequisites

First, install the required dependencies and generate the gRPC client:

```bash
go mod init nextblock-tip-floor-example
go get google.golang.org/grpc
go get github.com/gagliardetto/solana-go
# Clone and generate the proto client
git clone https://github.com/nextblock-ag/nextblock-proto
# Follow the Go generation instructions in the proto repo
```

## Example

<pre class="language-go"><code class="lang-go"><strong>package main
</strong>
import (
    "context"
    "crypto/x509"
    "fmt"
    "log"
    "sync"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/keepalive"
    // Import your generated proto client here
    // "path/to/your/generated/api"
)

<strong>// Available NextBlock endpoints
</strong>const (
    FrankfurtEndpoint = "frankfurt.nextblock.io:443"
    AmsterdamEndpoint = "amsterdam.nextblock.io:443"
    LondonEndpoint    = "london.nextblock.io:443"
    SingaporeEndpoint = "singapore.nextblock.io:443"
    NewYorkEndpoint   = "ny.nextblock.io:443"
    SaltLakeEndpoint  = "slc.nextblock.io:443"
    TokyoEndpoint     = "tokyo.nextblock.io:443"
)

<strong>// API key credentials for authentication
</strong>type ApiKeyCredentials struct {
    apiKey string
}

func NewApiKeyCredentials(apiKey string) *ApiKeyCredentials {
    return &#x26;ApiKeyCredentials{apiKey: apiKey}
}

func (a *ApiKeyCredentials) RequireTransportSecurity() bool {
    return true
}

func (a *ApiKeyCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "authorization": a.apiKey,
    }, nil
}

<strong>// Tip floor data structure
</strong>type TipFloorData struct {
    Time                         string
    LandedTips25thPercentile     float64
    LandedTips50thPercentile     float64
    LandedTips75thPercentile     float64
    LandedTips95thPercentile     float64
    LandedTips99thPercentile     float64
    EMALandedTips50thPercentile  float64
}

<strong>// Tip strategy for dynamic tip calculation
</strong>type TipStrategy struct {
    mu             sync.RWMutex
    conservativeTip uint64    // 25th percentile in lamports
    normalTip      uint64    // 50th percentile in lamports
    aggressiveTip  uint64    // 75th percentile in lamports
    priorityTip    uint64    // 95th percentile in lamports
    lastUpdated    time.Time
}

func NewTipStrategy() *TipStrategy {
    return &#x26;TipStrategy{
        conservativeTip: 0,   // Will be set from tip floor data
        normalTip:      0,    // Will be set from tip floor data
        aggressiveTip:  0,    // Will be set from tip floor data
        priorityTip:    0,    // Will be set from tip floor data
        lastUpdated:    time.Now(),
    }
}

func (ts *TipStrategy) UpdateFromTipFloor(tipFloor *TipFloorData) {
    ts.mu.Lock()
    defer ts.mu.Unlock()
    
    ts.conservativeTip = solToLamports(tipFloor.LandedTips25thPercentile)
    ts.normalTip = solToLamports(tipFloor.LandedTips50thPercentile)
    ts.aggressiveTip = solToLamports(tipFloor.LandedTips75thPercentile)
    ts.priorityTip = solToLamports(tipFloor.LandedTips95thPercentile)
    ts.lastUpdated = time.Now()
}

func (ts *TipStrategy) GetTip(priority string) uint64 {
    ts.mu.RLock()
    defer ts.mu.RUnlock()
    
    switch priority {
    case "conservative":
        return ts.conservativeTip
    case "normal":
        return ts.normalTip
    case "aggressive":
        return ts.aggressiveTip
    case "priority":
        return ts.priorityTip
    default:
        return ts.normalTip
    }
}

func solToLamports(sol float64) uint64 {
    return uint64(sol * 1_000_000_000)
}

<strong>// Connection setup with authentication
</strong>func connectToNextblock(endpoint, apiKey string, useTLS bool) (*grpc.ClientConn, error) {
    var opts []grpc.DialOption
    
    // Configure transport credentials
    if useTLS {
        pool, err := x509.SystemCertPool()
        if err != nil {
            return nil, fmt.Errorf("failed to get system cert pool: %w", err)
        }
        creds := credentials.NewClientTLSFromCert(pool, "")
        opts = append(opts, grpc.WithTransportCredentials(creds))
    } else {
        opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
    }

    // Keep-alive parameters
    kacp := keepalive.ClientParameters{
        Time:                time.Minute,
        Timeout:             15 * time.Second,
        PermitWithoutStream: true,
    }
    opts = append(opts, grpc.WithKeepaliveParams(kacp))
    
    // Add API key authentication
    opts = append(opts, grpc.WithPerRPCCredentials(NewApiKeyCredentials(apiKey)))

    // Establish connection
    conn, err := grpc.NewClient(endpoint, opts...)
    if err != nil {
        return nil, fmt.Errorf("failed to connect: %w", err)
    }

    return conn, nil
}

<strong>// Stream tip floor data with complete error handling
</strong>func streamTipFloor(endpoint, apiKey string, tipStrategy *TipStrategy) error {
    fmt.Printf("Connecting to NextBlock at %s...\n", endpoint)
    
    // Establish connection
    conn, err := connectToNextblock(endpoint, apiKey, true)
    if err != nil {
        return fmt.Errorf("connection failed: %w", err)
    }
    defer conn.Close()

    // Create API client
    // nextblockApiClient := api.NewApiClient(conn)
    
    fmt.Println("Successfully connected! Starting tip floor stream...")

    /* Uncomment when you have the generated API client
    // Create streaming request
    sub, err := nextblockApiClient.StreamTipFloor(context.TODO(), &#x26;api.TipFloorStreamRequest{
        UpdateFrequency: "1m", // Update every minute
    })
    if err != nil {
        return fmt.Errorf("failed to start tip floor stream: %w", err)
    }

    fmt.Println("Streaming tip floor data (Ctrl+C to stop):")
    
    for {
        tipFloorResponse, err := sub.Recv()
        if err != nil {
            return fmt.Errorf("stream error: %w", err)
        }

        // Convert protobuf response to our struct
        tipFloor := &#x26;TipFloorData{
            Time:                         tipFloorResponse.Time,
            LandedTips25thPercentile:     tipFloorResponse.LandedTips25ThPercentile,
            LandedTips50thPercentile:     tipFloorResponse.LandedTips50ThPercentile,
            LandedTips75thPercentile:     tipFloorResponse.LandedTips75ThPercentile,
            LandedTips95thPercentile:     tipFloorResponse.LandedTips95ThPercentile,
            LandedTips99thPercentile:     tipFloorResponse.LandedTips99ThPercentile,
            EMALandedTips50thPercentile:  tipFloorResponse.EmaLandedTips50ThPercentile,
        }

        // Process the tip floor update
        processTipFloorUpdate(tipFloor, tipStrategy)
    }
    */
    
    // Mock streaming for demonstration
    fmt.Println("Mock tip floor streaming (Ctrl+C to stop):")
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()
    
    for {
        select {
        case &#x3C;-ticker.C:
            // Generate mock tip floor data
            mockTipFloor := &#x26;TipFloorData{
                Time:                         time.Now().Format(time.RFC3339),
                LandedTips25thPercentile:     0.0011,
                LandedTips50thPercentile:     0.005000001,
                LandedTips75thPercentile:     0.01555,
                LandedTips95thPercentile:     0.09339195639999975,
                LandedTips99thPercentile:     0.4846427910400001,
                EMALandedTips50thPercentile:  0.005989477267191758,
            }
            
            processTipFloorUpdate(mockTipFloor, tipStrategy)
        }
    }
}

<strong>// Process tip floor updates and update strategy
</strong>func processTipFloorUpdate(tipFloor *TipFloorData, tipStrategy *TipStrategy) {
    fmt.Printf("\n=== Tip Floor Update at %s ===\n", tipFloor.Time)
    fmt.Printf("25th percentile: %.6f SOL\n", tipFloor.LandedTips25thPercentile)
    fmt.Printf("50th percentile: %.6f SOL\n", tipFloor.LandedTips50thPercentile)
    fmt.Printf("75th percentile: %.6f SOL\n", tipFloor.LandedTips75thPercentile)
    fmt.Printf("95th percentile: %.6f SOL\n", tipFloor.LandedTips95thPercentile)
    fmt.Printf("99th percentile: %.6f SOL\n", tipFloor.LandedTips99thPercentile)
    fmt.Printf("EMA 50th percentile: %.6f SOL\n", tipFloor.EMALandedTips50thPercentile)
    
    // Update tip strategy
    tipStrategy.UpdateFromTipFloor(tipFloor)
    
    // Display updated tip recommendations
    fmt.Printf("\n--- Updated Tip Recommendations ---\n")
    fmt.Printf("Conservative: %d lamports (low priority)\n", 
        tipStrategy.GetTip("conservative"))
    fmt.Printf("Normal:       %d lamports (standard priority)\n", 
        tipStrategy.GetTip("normal"))
    fmt.Printf("Aggressive:   %d lamports (high priority)\n", 
        tipStrategy.GetTip("aggressive"))
    fmt.Printf("Priority:     %d lamports (highest priority)\n", 
        tipStrategy.GetTip("priority"))
    fmt.Println("=====================================")
}

<strong>// Example of using dynamic tips in transaction submission
</strong>func exampleTransactionWithDynamicTip(tipStrategy *TipStrategy) {
    // Get current optimal tip based on desired priority level
    currentTip := tipStrategy.GetTip("normal")
    
    fmt.Printf("\nExample: Using dynamic tip based on current tip floor data\n")
    fmt.Printf("Current tip for normal priority: %d lamports\n", currentTip)
    fmt.Printf("Tip automatically adapts to network conditions\n")
    fmt.Printf("Higher tips = higher transaction priority\n")
    
    // Here you would use this tip amount in your transaction building
    // See submit-single-transactions.md for complete transaction examples
}

<strong>// Main function demonstrating complete usage
</strong>func main() {
    // Configuration
    apiKey := "&#x3C;your-api-key-here>"  // Replace with your actual API key
    endpoint := FrankfurtEndpoint              // Choose your preferred endpoint
    
    if apiKey == "&#x3C;your-api-key-here>" {
        log.Fatal("Please set your API key in the code")
    }
    
    // Initialize tip strategy
    tipStrategy := NewTipStrategy()
    
    // Start a goroutine to demonstrate using dynamic tips
    go func() {
        ticker := time.NewTicker(2 * time.Minute)
        defer ticker.Stop()
        
        for range ticker.C {
            exampleTransactionWithDynamicTip(tipStrategy)
        }
    }()
    
    // Start streaming tip floor data
    if err := streamTipFloor(endpoint, apiKey, tipStrategy); err != nil {
        log.Fatalf("Tip floor streaming failed: %v", err)
    }
}
</code></pre>

## Advanced Usage with Multiple Endpoints

<pre class="language-go"><code class="lang-go"><strong>// Connect to multiple endpoints for redundancy
</strong>func streamFromMultipleEndpoints(apiKey string, tipStrategy *TipStrategy) {
    endpoints := []string{
        FrankfurtEndpoint,
        AmsterdamEndpoint,
        NewYorkEndpoint,
    }
    
    var wg sync.WaitGroup
    
    for _, endpoint := range endpoints {
        wg.Add(1)
        go func(ep string) {
            defer wg.Done()
            
            fmt.Printf("Starting stream from %s\n", ep)
            if err := streamTipFloor(ep, apiKey, tipStrategy); err != nil {
                log.Printf("Stream from %s failed: %v", ep, err)
            }
        }(endpoint)
    }
    
    wg.Wait()
}

<strong>// Tip history tracking for trend analysis
</strong>type TipHistory struct {
    mu      sync.RWMutex
    history []TipFloorData
    maxSize int
}

func NewTipHistory(maxSize int) *TipHistory {
    return &#x26;TipHistory{
        history: make([]TipFloorData, 0, maxSize),
        maxSize: maxSize,
    }
}

func (th *TipHistory) Add(tipFloor TipFloorData) {
    th.mu.Lock()
    defer th.mu.Unlock()
    
    if len(th.history) >= th.maxSize {
        th.history = th.history[1:]
    }
    th.history = append(th.history, tipFloor)
}

func (th *TipHistory) GetTrend() float64 {
    th.mu.RLock()
    defer th.mu.RUnlock()
    
    if len(th.history) &#x3C; 2 {
        return 0.0
    }
    
    recent := th.history[len(th.history)-1]
    older := th.history[len(th.history)-2]
    
    return recent.LandedTips50thPercentile - older.LandedTips50thPercentile
}

<strong>// Smart tip calculation with trend analysis
</strong>func (ts *TipStrategy) GetSmartTip(priority string, tipHistory *TipHistory) uint64 {
    baseTip := ts.GetTip(priority)
    trend := tipHistory.GetTrend()
    
    // Adjust tip based on trend
    adjustmentFactor := 1.0
    if trend > 0.001 { // Tips increasing
        adjustmentFactor = 1.2
        fmt.Printf("Tips trending up (+%.6f), increasing tip by 20%%\n", trend)
    } else if trend &#x3C; -0.001 { // Tips decreasing
        adjustmentFactor = 0.9
        fmt.Printf("Tips trending down (%.6f), decreasing tip by 10%%\n", trend)
    }
    
    smartTip := uint64(float64(baseTip) * adjustmentFactor)
    return smartTip
}

func max(a, b uint64) uint64 {
    if a > b {
        return a
    }
    return b
}
</code></pre>

## Available Endpoints

Choose the endpoint closest to your location for optimal latency:

* **Frankfurt**: `frankfurt.nextblock.io:443` (Europe)
* **Amsterdam**: `amsterdam.nextblock.io:443` (Europe)
* **London**: `london.nextblock.io:443` (Europe)
* **Singapore**: `singapore.nextblock.io:443` (Asia)
* **Tokyo**: `tokyo.nextblock.io:443` (Asia)
* **New York**: `ny.nextblock.io:443` (US East)
* **Salt Lake City**: `slc.nextblock.io:443` (US West)

## Best Practices

1. **Choose optimal endpoint**: Use the endpoint closest to your location for best performance
2. **Use TLS by default**: Prefer secure connections unless you intentionally operate in a trusted internal environment
3. **Handle connection failures**: Implement retry logic and fallback endpoints
4. **Monitor tip trends**: Use historical data to make smarter tip decisions
5. **Update frequently**: Stream tip floor data continuously for best results
6. **Adapt tips dynamically**: Adjust tip amounts based on current network conditions
7. **Match priority to urgency**: Use appropriate tip levels based on your transaction urgency needs
8. **Implement graceful shutdown**: Handle interruption signals properly
9. **Log important events**: Track tip floor updates and connection issues


# QUIC Transaction Submission

Use QUIC when you want to send signed transaction bytes to NextBlock with minimal transport overhead from Go.

The pattern is simple:

1. Dial a regional endpoint such as `frankfurt.nextblock.io:11100`
2. Authenticate once on a bidirectional stream
3. Reuse the same QUIC connection and open one unidirectional stream per transaction

## Example

```go
package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"os"
	"time"

	quic "github.com/quic-go/quic-go"
)

const (
	authOK    byte = 0x00
	maxTxSize      = 1232
)

type NextblockQuicClient struct {
	conn quic.Connection
}

func Connect(ctx context.Context, serverAddr string, apiKey string) (*NextblockQuicClient, error) {
	host, _, err := net.SplitHostPort(serverAddr)
	if err != nil {
		return nil, fmt.Errorf("invalid server address: %w", err)
	}

	conn, err := quic.DialAddr(ctx, serverAddr, &tls.Config{
		ServerName: host,
		NextProtos: []string{"nb-tx/1"},
		MinVersion: tls.VersionTLS13,
	}, &quic.Config{
		KeepAlivePeriod: 15 * time.Second,
		MaxIdleTimeout:  60 * time.Second,
	})
	if err != nil {
		return nil, fmt.Errorf("quic dial failed: %w", err)
	}

	authStream, err := conn.OpenStreamSync(ctx)
	if err != nil {
		conn.CloseWithError(1, "auth stream failed")
		return nil, fmt.Errorf("open auth stream: %w", err)
	}

	if _, err := authStream.Write([]byte(apiKey)); err != nil {
		conn.CloseWithError(1, "auth write failed")
		return nil, fmt.Errorf("write api key: %w", err)
	}
	if err := authStream.Close(); err != nil {
		conn.CloseWithError(1, "auth close failed")
		return nil, fmt.Errorf("close auth stream: %w", err)
	}

	response := make([]byte, 1)
	if _, err := io.ReadFull(authStream, response); err != nil {
		conn.CloseWithError(1, "auth read failed")
		return nil, fmt.Errorf("read auth response: %w", err)
	}
	if response[0] != authOK {
		conn.CloseWithError(1, "auth rejected")
		return nil, fmt.Errorf("authentication rejected")
	}

	return &NextblockQuicClient{conn: conn}, nil
}

func (c *NextblockQuicClient) SendTransaction(ctx context.Context, rawTx []byte) error {
	if len(rawTx) > maxTxSize {
		return fmt.Errorf("transaction too large: %d", len(rawTx))
	}

	stream, err := c.conn.OpenUniStreamSync(ctx)
	if err != nil {
		return fmt.Errorf("open tx stream: %w", err)
	}

	if _, err := stream.Write(rawTx); err != nil {
		return fmt.Errorf("write tx bytes: %w", err)
	}

	return stream.Close()
}

func (c *NextblockQuicClient) Close() error {
	return c.conn.CloseWithError(0, "client closing")
}

func main() {
	ctx := context.Background()
	apiKey := os.Getenv("NEXTBLOCK_API_KEY")
	if apiKey == "" {
		panic("set NEXTBLOCK_API_KEY before running")
	}

	client, err := Connect(ctx, "frankfurt.nextblock.io:11100", apiKey)
	if err != nil {
		panic(err)
	}
	defer client.Close()

	// Replace this with the serialized bytes of your signed Solana transaction.
	// Example with solana-go: rawTx, err := tx.MarshalBinary()
	rawTx := []byte{0, 1, 2, 3}

	if err := client.SendTransaction(ctx, rawTx); err != nil {
		panic(err)
	}

	fmt.Println("transaction queued")
}
```

## What To Replace

* Swap `frankfurt.nextblock.io:11100` for your nearest region.
* Replace `rawTx` with the serialized bytes of your signed transaction.
* Keep one `NextblockQuicClient` alive and reuse it instead of reconnecting per send.

## Notes

* Send raw transaction bytes, not base64.
* Each transaction goes on its own unidirectional stream.
* QUIC does not support the extra gRPC submission flags or atomic bundle submission.
* If you already build transactions with `solana-go`, serialize the signed transaction and pass those bytes into `SendTransaction()`.

See [QUIC Transaction Submission](/api/quic-transaction-submission) for the full endpoint list and protocol summary.


# Submit single transactions

This example shows how to submit a single transaction to NextBlock using the gRPC API. We'll create a transaction with a tip and a useful instruction.

If you want to send raw signed transaction bytes over QUIC instead of gRPC, see [QUIC Transaction Submission](/api/examples/golang/quic).

This example shows transaction construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

## Example

```go
package main

import (
    "context"
    "fmt"
    "log"
    "math/rand"
    "time"
    
    "github.com/gagliardetto/solana-go"
    "github.com/gagliardetto/solana-go/programs/system"
    "github.com/gagliardetto/solana-go/rpc"
    // Import your generated proto client
    // "path/to/your/generated/api"
)

// NextBlock tip wallets - use these for tipping
var NextblockTipWallets = []solana.PublicKey{
    solana.MustPublicKeyFromBase58("NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid"),
    solana.MustPublicKeyFromBase58("nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"),
    solana.MustPublicKeyFromBase58("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"),
    solana.MustPublicKeyFromBase58("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"),
    solana.MustPublicKeyFromBase58("NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X"),
    solana.MustPublicKeyFromBase58("NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb"),
    solana.MustPublicKeyFromBase58("neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At"),
    solana.MustPublicKeyFromBase58("nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG"),
}

// GetRandomNextblockTipWallet returns a random tip wallet for load balancing
func GetRandomNextblockTipWallet() solana.PublicKey {
    randVal := rand.IntN(len(NextblockTipWallets))
    return NextblockTipWallets[randVal]
}

// Calculate optimal tip based on priority and current tip floor
func calculateOptimalTip(priority string) uint64 {
    // This should get tip from your tip floor stream
    // Higher tips = higher priority
    // Adapt based on current network conditions
    
    // Get current tip floor data from your streaming connection
    // tipFloor := getCurrentTipFloor()
    
    // Use tip floor percentiles to determine appropriate tip
    // Example: use 25th percentile for low priority, 50th for normal, etc.
    // Return the calculated tip based on current network conditions
    
    // For now, return 0 - you must implement tip floor integration
    return 0 // Replace with actual tip floor calculation
}

// BuildAndSubmitTransaction creates and submits a transaction with proper tip
func BuildAndSubmitTransaction(
    nextblockApiClient interface{}, // Replace with your generated API client type
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
    recipientPubkey solana.PublicKey,
    transferAmount uint64,
    tipAmount uint64,
) error {
    // Get latest blockhash for transaction
    bh, err := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
    if err != nil {
        return fmt.Errorf("failed to get latest blockhash: %w", err)
    }

    // Build transaction
    txBuilder := solana.NewTransactionBuilder()
    txBuilder.SetFeePayer(signerPrivateKey.PublicKey())
    txBuilder.SetRecentBlockHash(bh.Value.Blockhash)

    // Add tip instruction (IMPORTANT: This helps prioritize your transaction)
    tipInstruction := system.NewTransferInstruction(
        tipAmount,
        signerPrivateKey.PublicKey(),
        GetRandomNextblockTipWallet(),
    ).Build()
    txBuilder.AddInstruction(tipInstruction)

    // Add your main instruction (replace with your actual business logic)
    transferInstruction := system.NewTransferInstruction(
        transferAmount,
        signerPrivateKey.PublicKey(),
        recipientPubkey,
    ).Build()
    txBuilder.AddInstruction(transferInstruction)

    // Build and sign transaction
    tx, err := txBuilder.Build()
    if err != nil {
        return fmt.Errorf("failed to build transaction: %w", err)
    }

    _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
        if key.Equals(signerPrivateKey.PublicKey()) {
            return &signerPrivateKey
        }
        return nil
    })
    if err != nil {
        return fmt.Errorf("failed to sign transaction: %w", err)
    }

    // Convert to base64 for submission
    base64EncodedTransaction := tx.ToBase64()

    // Configure submission options
    frontRunningProtection := false // Set to true to protect from MEV
    revertOnFail := false          // Set to true to revert if transaction fails
    disableRetries := false        // Set to true to disable automatic retries
    snipeTransaction := false      // Set to true for high-priority submission

    // Submit transaction to NextBlock
    /* Uncomment when you have the generated API client
    submitResponse, err := nextblockApiClient.PostSubmitV2(context.TODO(), &api.PostSubmitRequest{
        Transaction:            &api.TransactionMessage{Content: base64EncodedTransaction},
        SkipPreFlight:          true, // Skip preflight checks for faster submission
        SnipeTransaction:       &snipeTransaction,
        FrontRunningProtection: &frontRunningProtection,
        DisableRetries:         &disableRetries,
        RevertOnFail:          &revertOnFail,
    })
    if err != nil {
        return fmt.Errorf("failed to submit transaction: %w", err)
    }

    fmt.Printf("Transaction submitted successfully!\n")
    fmt.Printf("Signature: %s\n", submitResponse.Signature)
    fmt.Printf("UUID: %s\n", submitResponse.Uuid)
    */

    fmt.Printf("Local transaction built successfully: %s\n", tx.Signatures[0].String())
    return nil
}

func main() {
    // Initialize random seed
    rand.Seed(time.Now().UnixNano())

    // Set up your configuration
    signerPrivateKey := solana.MustPrivateKeyFromBase58("your-private-key-here")
    recipientPubkey := solana.MustPublicKeyFromBase58("recipient-public-key-here")
    rpcEndpoint := "https://api.mainnet-beta.solana.com" // Use your preferred RPC
    
    // Initialize RPC client
    rpcClient := rpc.New(rpcEndpoint)

    // Connect to NextBlock (see connection.md for full setup)
    // conn, err := ConnectToNextblock("frankfurt.nextblock.io:443", "your-api-key", true)
    // if err != nil {
    //     log.Fatal(err)
    // }
    // defer conn.Close()
    // nextblockApiClient := api.NewApiClient(conn)

    // Submit transaction with dynamic tip based on priority
    err := BuildAndSubmitTransaction(
        nil, // nextblockApiClient,
        rpcClient,
        signerPrivateKey,
        recipientPubkey,
        10000,    // Transfer amount
        calculateOptimalTip("normal"),  // Dynamic tip based on tip floor
    )
    if err != nil {
        log.Fatal(err)
    }
}
```

## Key Features Explained

### Tip Amount Calculation

Use the tip floor API to determine appropriate tip amounts:

```go
// Get current tip floor data (see tip-floor-stream.md for streaming version)
func GetOptimalTipAmount(nextblockApiClient interface{}) uint64 {
    // This would call the tip floor API to get current percentiles
    // For now, use a reasonable default
    return 1000000 // 0.001 SOL
}
```

### Transaction Options

* **SkipPreFlight**: Set to `true` for faster submission (recommended)
* **FrontRunningProtection**: Protects against MEV attacks
* **DisableRetries**: Disable automatic retries if you handle them yourself
* **RevertOnFail**: Revert transaction state if it fails
* **SnipeTransaction**: High-priority submission mode

### Error Handling

Always implement proper error handling:

```go
if err != nil {
    // Log the error with context
    log.Printf("Transaction submission failed: %v", err)
    
    // Implement retry logic if needed
    // Check if error is retryable
    // Wait and retry with exponential backoff
}
```

### Best Practices

1. **Always include tips**: NextBlock prioritizes transactions based on tip amounts - higher tips = higher priority
2. **Use random tip wallets**: Distribute load across multiple tip addresses
3. **Adapt tips to network conditions**: Use the tip floor streaming API to dynamically adjust your tips based on current network conditions
4. **Choose appropriate priority levels**: Use different tip levels based on your transaction urgency needs
5. **Handle errors gracefully**: Implement retry logic for network issues
6. **Use appropriate RPC endpoints**: Choose reliable RPC providers for blockhash retrieval
7. **Keep connections alive**: Reuse gRPC connections for better performance


# Submit batched transactions

Submit 2-4 transactions as an atomic bundle to NextBlock. Batched transactions are processed as Jito bundles, meaning either all transactions succeed and land, or none of them do.

This example shows bundle construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

## Example

```go
package main

import (
    "context"
    "fmt"
    "log"
    "math/rand"
    "time"
    
    "github.com/gagliardetto/solana-go"
    "github.com/gagliardetto/solana-go/programs/system"
    "github.com/gagliardetto/solana-go/rpc"
    // Import your generated proto client
    // "path/to/your/generated/api"
)

// NextBlock tip wallets - use these for tipping
var NextblockTipWallets = []solana.PublicKey{
    solana.MustPublicKeyFromBase58("NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid"),
    solana.MustPublicKeyFromBase58("nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"),
    solana.MustPublicKeyFromBase58("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"),
    solana.MustPublicKeyFromBase58("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"),
    solana.MustPublicKeyFromBase58("NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X"),
    solana.MustPublicKeyFromBase58("NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb"),
    solana.MustPublicKeyFromBase58("neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At"),
    solana.MustPublicKeyFromBase58("nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG"),
}

// GetRandomNextblockTipWallet returns a random tip wallet for load balancing
func GetRandomNextblockTipWallet() solana.PublicKey {
    randVal := rand.IntN(len(NextblockTipWallets))
    return NextblockTipWallets[randVal]
}

// BuildTransaction creates a single transaction with tip and instructions
func BuildTransaction(
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
    blockhash solana.Hash,
    tipAmount uint64,
    instructions []solana.Instruction,
) (*solana.Transaction, error) {
    txBuilder := solana.NewTransactionBuilder()
    txBuilder.SetFeePayer(signerPrivateKey.PublicKey())
    txBuilder.SetRecentBlockHash(blockhash)

    // Add tip instruction first (recommended practice)
    tipInstruction := system.NewTransferInstruction(
        tipAmount,
        signerPrivateKey.PublicKey(),
        GetRandomNextblockTipWallet(),
    ).Build()
    txBuilder.AddInstruction(tipInstruction)

    // Add all provided instructions
    for _, instruction := range instructions {
        txBuilder.AddInstruction(instruction)
    }

    // Build and sign transaction
    tx, err := txBuilder.Build()
    if err != nil {
        return nil, fmt.Errorf("failed to build transaction: %w", err)
    }

    _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
        if key.Equals(signerPrivateKey.PublicKey()) {
            return &signerPrivateKey
        }
        return nil
    })
    if err != nil {
        return nil, fmt.Errorf("failed to sign transaction: %w", err)
    }

    return tx, nil
}

// BuildAndSubmitBatchTransactions creates and submits multiple transactions as a bundle
func BuildAndSubmitBatchTransactions(
    nextblockApiClient interface{}, // Replace with your generated API client type
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
) error {
    // Get latest blockhash (same for all transactions in bundle)
    bh, err := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
    if err != nil {
        return fmt.Errorf("failed to get latest blockhash: %w", err)
    }

    var transactions []*solana.Transaction
    
    // Transaction 1: Setup transaction
    setupInstructions := []solana.Instruction{
        system.NewTransferInstruction(
            100000, // 0.0001 SOL
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("recipient1-public-key-here"),
        ).Build(),
    }
    
    tx1, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        500000, // 0.0005 SOL tip
        setupInstructions,
    )
    if err != nil {
        return fmt.Errorf("failed to build transaction 1: %w", err)
    }
    transactions = append(transactions, tx1)

    // Transaction 2: Main operation
    mainInstructions := []solana.Instruction{
        system.NewTransferInstruction(
            200000, // 0.0002 SOL
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("recipient2-public-key-here"),
        ).Build(),
    }
    
    tx2, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        1000000, // 0.001 SOL tip (higher for main transaction)
        mainInstructions,
    )
    if err != nil {
        return fmt.Errorf("failed to build transaction 2: %w", err)
    }
    transactions = append(transactions, tx2)

    // Transaction 3: Cleanup transaction (optional)
    cleanupInstructions := []solana.Instruction{
        system.NewTransferInstruction(
            50000, // 0.00005 SOL
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("recipient3-public-key-here"),
        ).Build(),
    }
    
    tx3, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        500000, // 0.0005 SOL tip
        cleanupInstructions,
    )
    if err != nil {
        return fmt.Errorf("failed to build transaction 3: %w", err)
    }
    transactions = append(transactions, tx3)

    // Convert transactions to base64 for submission
    var entries []*interface{} // Replace with your generated API types
    
    for i, tx := range transactions {
        base64EncodedTransaction := tx.ToBase64()
        fmt.Printf("Transaction %d signature: %s\n", i+1, tx.Signatures[0].String())
        
        // Create entry for batch submission
        /* Uncomment when you have the generated API client
        entry := &api.PostSubmitRequestEntry{
            Transaction: &api.TransactionMessage{
                Content: base64EncodedTransaction,
            },
        }
        entries = append(entries, entry)
        */
        _ = base64EncodedTransaction // Placeholder to avoid unused variable error
    }

    // Submit batch to NextBlock
    /* Uncomment when you have the generated API client
    submitResponse, err := nextblockApiClient.PostSubmitBatchV2(context.TODO(), &api.PostSubmitBatchRequest{
        Entries: entries,
    })
    if err != nil {
        return fmt.Errorf("failed to submit batch: %w", err)
    }

    fmt.Printf("Batch submitted successfully!\n")
    fmt.Printf("Bundle signature: %s\n", submitResponse.Signature)
    */

    fmt.Printf("Local bundle prepared with %d transactions.\n", len(transactions))
    return nil
}

// Example: Arbitrage Bundle
func BuildArbitrageBundleExample(
    nextblockApiClient interface{},
    rpcClient *rpc.Client,
    signerPrivateKey solana.PrivateKey,
) error {
    // Get blockhash
    bh, err := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
    if err != nil {
        return fmt.Errorf("failed to get latest blockhash: %w", err)
    }

    var transactions []*solana.Transaction

    // Transaction 1: Buy on DEX A
    buyInstructions := []solana.Instruction{
        // Add your DEX buy instructions here
        system.NewTransferInstruction(
            1000000, // Placeholder instruction
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("dex-a-address"),
        ).Build(),
    }
    
    buyTx, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        2000000, // Higher tip for arbitrage
        buyInstructions,
    )
    if err != nil {
        return err
    }
    transactions = append(transactions, buyTx)

    // Transaction 2: Sell on DEX B
    sellInstructions := []solana.Instruction{
        // Add your DEX sell instructions here
        system.NewTransferInstruction(
            1000000, // Placeholder instruction
            signerPrivateKey.PublicKey(),
            solana.MustPublicKeyFromBase58("dex-b-address"),
        ).Build(),
    }
    
    sellTx, err := BuildTransaction(
        rpcClient,
        signerPrivateKey,
        bh.Value.Blockhash,
        2000000, // Higher tip for arbitrage
        sellInstructions,
    )
    if err != nil {
        return err
    }
    transactions = append(transactions, sellTx)

    fmt.Printf("Arbitrage bundle with %d transactions prepared\n", len(transactions))
    // Submit logic similar to above...
    return nil
}

func main() {
    // Initialize random seed
    rand.Seed(time.Now().UnixNano())

    // Configuration
    signerPrivateKey := solana.MustPrivateKeyFromBase58("your-private-key-here")
    rpcEndpoint := "https://api.mainnet-beta.solana.com"
    
    // Initialize RPC client
    rpcClient := rpc.New(rpcEndpoint)

    // Connect to NextBlock (see connection.md for full setup)
    // conn, err := ConnectToNextblock("frankfurt.nextblock.io:443", "your-api-key", true)
    // if err != nil {
    //     log.Fatal(err)
    // }
    // defer conn.Close()
    // nextblockApiClient := api.NewApiClient(conn)

    // Submit batch transactions
    err := BuildAndSubmitBatchTransactions(
        nil, // nextblockApiClient,
        rpcClient,
        signerPrivateKey,
    )
    if err != nil {
        log.Fatal(err)
    }

    // Example: Submit arbitrage bundle
    err = BuildArbitrageBundleExample(
        nil, // nextblockApiClient,
        rpcClient,
        signerPrivateKey,
    )
    if err != nil {
        log.Fatal(err)
    }
}
```

## Batch Transaction Best Practices

### Bundle Size Limits

* **Minimum**: 2 transactions
* **Maximum**: 4 transactions
* **Optimal**: 2-3 transactions for better success rates

### Transaction Ordering

1. **Setup transactions**: Account creation, token account setup
2. **Main transactions**: Core business logic
3. **Cleanup transactions**: Close accounts, collect fees

### Tip Strategy for Bundles

* **Higher tips**: Use higher tips for atomic operations like arbitrage
* **Distributed tips**: Each transaction should have its own tip
* **Progressive tipping**: Increase tips for more important transactions

### Error Handling

```go
// Check if bundle was included atomically
func CheckBundleStatus(signature string) {
    // Implement bundle status checking
    // All transactions should have the same block slot if successful
}
```

### Common Use Cases

1. **Arbitrage**: Buy low on one DEX, sell high on another
2. **Complex DeFi operations**: Setup → Trade → Cleanup
3. **NFT operations**: Create → Mint → Transfer
4. **Token operations**: Create mint → Create accounts → Transfer

### Bundle Failure Scenarios

* **Insufficient balance**: Ensure all accounts have enough SOL
* **Account conflicts**: Avoid write conflicts between transactions
* **Instruction limits**: Keep total compute units under limits
* **Blockhash expiry**: Use fresh blockhashes (valid for \~60 seconds)


# Http

Complete HTTP REST API examples for integrating with NextBlock.

HTTP is the simplest way to integrate if you want to submit transactions with JSON requests. If you want the full typed client flow, use the gRPC examples. If you want the lowest-latency submission path, use [QUIC Transaction Submission](/api/quic-transaction-submission).

## API Endpoints

Choose the endpoint closest to your location:

* **Frankfurt**: `https://frankfurt.nextblock.io`
* **Amsterdam**: `https://amsterdam.nextblock.io`
* **London**: `https://london.nextblock.io`
* **Singapore**: `https://singapore.nextblock.io`
* **Tokyo**: `https://tokyo.nextblock.io`
* **New York**: `https://ny.nextblock.io`
* **Salt Lake City**: `https://slc.nextblock.io`
* **Dublin**: `https://dublin.nextblock.io`
* **Vilnius**: `https://vilnius.nextblock.io`

## Authentication

All API requests require an API key in the Authorization header:

```http
Authorization: your-api-key-here
```

## Single Transaction Submission

### POST /api/v2/submit

```http
POST /api/v2/submit HTTP/1.1
Host: frankfurt.nextblock.io
Content-Type: application/json
Authorization: your-api-key-here

{
  "transaction": {
    "content": "base-64-encoded-transaction"
  },
  "frontRunningProtection": false
}
```

**Response:**

```json
{
  "signature": "transaction-signature",
  "uuid": "jito-bundle-uuid"
}
```

## Batched Transaction Submission

### POST /api/v2/submit-batch

```http
POST /api/v2/submit-batch HTTP/1.1
Host: frankfurt.nextblock.io
Content-Type: application/json
Authorization: your-api-key-here

{
  "entries": [
    {
      "transaction": {
        "content": "base-64-encoded-transaction-1"
      }
    },
    {
      "transaction": {
        "content": "base-64-encoded-transaction-2"
      }
    }
  ]
}
```

**Response:**

```json
{
  "signature": "bundle-signature"
}
```

## Tip Floor API

### GET /api/v2/tipfloor

```http
GET /api/v2/tipfloor HTTP/1.1
Host: frankfurt.nextblock.io
Authorization: your-api-key-here
```

**Response:**

```json
{
  "time": "2025-05-13T10:41:45Z",
  "landed_tips_25th_percentile": 0.0011,
  "landed_tips_50th_percentile": 0.005000001,
  "landed_tips_75th_percentile": 0.01555,
  "landed_tips_95th_percentile": 0.09339195639999975,
  "landed_tips_99th_percentile": 0.4846427910400001,
  "ema_landed_tips_50th_percentile": 0.005989477267191758
}
```

## cURL Examples

```bash
# Submit single transaction
curl -X POST https://frankfurt.nextblock.io/api/v2/submit \
  -H "Content-Type: application/json" \
  -H "Authorization: your-api-key-here" \
  -d '{
    "transaction": {
      "content": "AjF+B...BCQ=="
    },
    "frontRunningProtection": false
  }'

# Submit batched transactions
curl -X POST https://frankfurt.nextblock.io/api/v2/submit-batch \
  -H "Content-Type: application/json" \
  -H "Authorization: your-api-key-here" \
  -d '{
    "entries": [
      {
        "transaction": {
          "content": "ASrTNkPOT...BCQ=="
        }
      },
      {
        "transaction": {
          "content": "AVFRplUyy...BCQ=="
        }
      }
    ]
  }'

# Get tip floor data
curl -X GET https://frankfurt.nextblock.io/api/v2/tipfloor \
  -H "Authorization: your-api-key-here"
```

The language sections in this docs set focus mainly on gRPC and QUIC examples. Use this page as the canonical HTTP request reference.


# Rust

Complete Rust examples for integrating with NextBlock's gRPC API and QUIC transaction submission using the Solana SDK.

## Overview

These examples demonstrate how to:

* Establish secure gRPC connections with authentication
* Submit raw transaction bytes over QUIC for the lowest transport overhead
* Submit single and batched transactions with proper tipping
* Stream real-time tip floor data for dynamic tip optimization
* Maintain persistent connections with keepalive mechanisms

## Prerequisites

Add these dependencies to your `Cargo.toml`:

```toml
[dependencies]
tonic = "0.10"
tokio = { version = "1.0", features = ["full"] }
solana-sdk = "1.17"
solana-client = "1.17"
base64 = "0.21"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.8"
chrono = { version = "0.4", features = ["serde"] }
once_cell = "1.19"

# Add your generated proto dependencies here
# nextblock-proto = { path = "./generated" }
```

## Examples

### Core Examples

* [Connection](/api/examples/rust/connection) - Establish gRPC connections with authentication
* [QUIC Transaction Submission](/api/examples/rust/quic) - Send raw signed transaction bytes over QUIC
* [Submit Single Transaction](/api/examples/rust/submit-single-transactions) - Send individual transactions with tips
* [Submit Batched Transactions](/api/examples/rust/submit-batched-transactions) - Send atomic transaction bundles
* [Tip Floor Stream](/api/examples/rust/tip-floor-stream) - Stream real-time tip floor data
* [Keepalive](/api/examples/rust/keepalive) - Maintain persistent connections

## Quick Start

1. **Generate gRPC client** from [nextblock-proto](https://github.com/nextblock-ag/nextblock-proto)
2. **Set up your environment** with API key and endpoint
3. **Start with connection example** to establish authenticated gRPC connection
4. **Use tip floor streaming** to optimize transaction tips dynamically
5. **Submit transactions** using single or batched submission methods

## Key Features

### Authentication

All examples include proper API key authentication using Tonic's interceptor pattern.

### Error Handling

Comprehensive error handling with retry logic and graceful degradation.

### Connection Management

Persistent connections with keepalive, health monitoring, and automatic recovery.

### Dynamic Tipping

Real-time tip optimization based on current network conditions from tip floor API.

### Transaction Building

Complete transaction building with Solana SDK integration and proper signing.

## Best Practices

1. **Use TLS in production** - Always enable TLS for production environments
2. **Implement keepalive** - Maintain persistent connections for better performance
3. **Monitor tip floors** - Use streaming API to adjust tips dynamically
4. **Handle errors gracefully** - Implement proper retry logic with exponential backoff
5. **Validate inputs** - Always validate public keys and transaction parameters
6. **Use appropriate endpoints** - Choose the endpoint closest to your location


# Connection

Establish a connection to NextBlock's gRPC API using Tonic.

This page shows the connection pattern and authentication interceptor. Replace the placeholder generated client type with the client generated from [`nextblock-proto`](https://github.com/nextblock-ag/nextblock-proto).

## Prerequisites

Add these dependencies to your `Cargo.toml`:

```toml
[dependencies]
tonic = "0.10"
tokio = { version = "1.0", features = ["full"] }
solana-sdk = "1.17"
solana-client = "1.17"
base64 = "0.21"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.8"

# Add your generated proto dependencies here
# nextblock-proto = { path = "./generated" }
```

## Connection Setup

<pre class="language-rust"><code class="lang-rust"><strong>use std::time::Duration;
</strong>use tonic::{
    transport::{Channel, ClientTlsConfig, Endpoint},
    metadata::{MetadataValue, MetadataMap},
    Request, Status,
};
use tokio::time;

<strong>// Authentication interceptor for API key
</strong>#[derive(Clone)]
pub struct AuthInterceptor {
    api_key: MetadataValue&#x3C;tonic::metadata::Ascii>,
}

impl AuthInterceptor {
    pub fn new(api_key: String) -> Result&#x3C;Self, tonic::metadata::errors::InvalidMetadataValue> {
        let api_key = MetadataValue::try_from(api_key)?;
        Ok(Self { api_key })
    }
}

impl tonic::service::Interceptor for AuthInterceptor {
    fn call(&#x26;mut self, mut request: Request&#x3C;()>) -> Result&#x3C;Request&#x3C;()>, Status> {
        request.metadata_mut().insert("authorization", self.api_key.clone());
        Ok(request)
    }
}

<strong>// Connection configuration
</strong>pub struct NextBlockConfig {
    pub endpoint: String,
    pub api_key: String,
    pub use_tls: bool,
    pub timeout: Duration,
}

impl Default for NextBlockConfig {
    fn default() -> Self {
        Self {
            endpoint: "https://frankfurt.nextblock.io".to_string(),
            api_key: String::new(),
            use_tls: true,
            timeout: Duration::from_secs(30),
        }
    }
}

<strong>// Establish connection to NextBlock
</strong>pub async fn connect_to_nextblock(config: NextBlockConfig) -> Result&#x3C;Channel, Box&#x3C;dyn std::error::Error>> {
    let mut endpoint = Endpoint::from_shared(config.endpoint)?
        .timeout(config.timeout)
        .tcp_keepalive(Some(Duration::from_secs(60)))
        .http2_keep_alive_interval(Some(Duration::from_secs(30)))
        .keep_alive_timeout(Duration::from_secs(15))
        .keep_alive_while_idle(true);

<strong>    // Configure TLS if enabled
</strong>    if config.use_tls {
        let tls_config = ClientTlsConfig::new()
            .with_native_roots();
        endpoint = endpoint.tls_config(tls_config)?;
    }

<strong>    // Create the channel
</strong>    let channel = endpoint.connect().await?;
    Ok(channel)
}

<strong>// Create authenticated client
</strong>pub async fn create_nextblock_client(
    config: NextBlockConfig,
) -> Result&#x3C;/* YourGeneratedClient */(), Box&#x3C;dyn std::error::Error>> {
    let channel = connect_to_nextblock(config.clone()).await?;
    
<strong>    // Create authentication interceptor
</strong>    let auth_interceptor = AuthInterceptor::new(config.api_key)?;
    
<strong>    // Create the client with authentication
</strong>    // let client = YourGeneratedApiClient::with_interceptor(channel, auth_interceptor);
    
    // Return the client
    // Ok(client)
    Ok(())
}
</code></pre>

## Usage Example

<pre class="language-rust"><code class="lang-rust"><strong>#[tokio::main]
</strong>async fn main() -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // Configure connection
</strong>    let config = NextBlockConfig {
        endpoint: "https://frankfurt.nextblock.io".to_string(),
        api_key: "&#x3C;your-api-key-here>".to_string(),
        use_tls: true,
        timeout: Duration::from_secs(30),
    };

<strong>    // Create authenticated client
</strong>    let client = create_nextblock_client(config).await?;
    
<strong>    // Use the client for API calls
</strong>    // See other examples for specific usage patterns
    
    println!("Successfully connected to NextBlock!");
    Ok(())
}
</code></pre>

## Connection Best Practices

1. **Use TLS in production**: Always enable TLS for production environments
2. **Configure keepalive**: HTTP/2 keepalive helps maintain persistent connections
3. **Set appropriate timeouts**: Configure timeouts based on your use case
4. **Handle authentication**: Use the interceptor pattern for API key authentication
5. **Error handling**: Implement proper error handling and retry logic

## Available Endpoints

* **Frankfurt**: `frankfurt.nextblock.io` (Europe)
* **Amsterdam**: `amsterdam.nextblock.io` (Europe)
* **London**: `london.nextblock.io` (Europe)
* **Singapore**: `singapore.nextblock.io` (Asia)
* **Tokyo**: `tokyo.nextblock.io` (Asia)
* **New York**: `ny.nextblock.io` (US East)
* **Salt Lake City**: `slc.nextblock.io` (US West)
* **Dublin**: `dublin.nextblock.io` (Europe)
* **Vilnius**: `vilnius.nextblock.io` (Europe)

## Environment Configuration

<pre class="language-rust"><code class="lang-rust"><strong>use std::env;
</strong>
pub fn config_from_env() -> NextBlockConfig {
    NextBlockConfig {
        endpoint: env::var("NEXTBLOCK_ENDPOINT")
            .unwrap_or_else(|_| "https://frankfurt.nextblock.io".to_string()),
        api_key: env::var("NEXTBLOCK_API_KEY")
            .expect("NEXTBLOCK_API_KEY must be set"),
        use_tls: env::var("NEXTBLOCK_USE_TLS")
            .map(|v| v.to_lowercase() == "true")
            .unwrap_or(true),
        timeout: Duration::from_secs(
            env::var("NEXTBLOCK_TIMEOUT")
                .and_then(|v| v.parse().ok())
                .unwrap_or(30)
        ),
    }
}
</code></pre>


# QUIC Transaction Submission

Use QUIC when you want the lowest-overhead path for sending signed Solana transaction bytes to NextBlock from Rust.

The flow is:

1. Connect to a regional QUIC endpoint such as `london.nextblock.io:11100`
2. Authenticate once with your API key on a bidirectional stream
3. Reuse the connection and send each transaction on its own unidirectional stream

## Example

```rust
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use anyhow::{anyhow, Context, Result};
use quinn::{ClientConfig, Connection, Endpoint, TransportConfig};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

const ALPN_NB_TX: &[u8] = b"nb-tx/1";
const AUTH_OK: u8 = 0x00;
const MAX_TX_SIZE: usize = 1232;

pub struct NextblockQuicClient {
    // Keep the endpoint alive for as long as the connection is in use.
    endpoint: Endpoint,
    connection: Connection,
}

impl NextblockQuicClient {
    pub async fn connect(server_addr: &str, api_key: &str) -> Result<Self> {
        let endpoint = create_endpoint()?;

        let addr: SocketAddr = tokio::net::lookup_host(server_addr)
            .await
            .context("dns lookup failed")?
            .next()
            .ok_or_else(|| anyhow!("no addresses found for {server_addr}"))?;

        let server_name = server_addr
            .split(':')
            .next()
            .ok_or_else(|| anyhow!("invalid server address"))?;

        let connection = endpoint
            .connect(addr, server_name)?
            .await
            .context("quic handshake failed")?;

        let (mut send, mut recv) = connection
            .open_bi()
            .await
            .context("failed to open auth stream")?;

        send.write_all(api_key.as_bytes())
            .await
            .context("failed to send api key")?;
        send.finish().context("failed to close auth stream")?;

        let mut response = [0u8; 1];
        recv.read_exact(&mut response)
            .await
            .context("failed to read auth response")?;

        if response[0] != AUTH_OK {
            return Err(anyhow!("authentication rejected"));
        }

        Ok(Self { endpoint, connection })
    }

    pub async fn send_transaction(&self, raw_tx: &[u8]) -> Result<()> {
        if raw_tx.len() > MAX_TX_SIZE {
            return Err(anyhow!("transaction too large: {}", raw_tx.len()));
        }

        let mut send = self
            .connection
            .open_uni()
            .await
            .context("failed to open tx stream")?;

        send.write_all(raw_tx)
            .await
            .context("failed to write transaction")?;
        send.finish().context("failed to finish tx stream")?;

        Ok(())
    }
}

impl Drop for NextblockQuicClient {
    fn drop(&mut self) {
        self.connection.close(quinn::VarInt::from_u32(0), b"client closing");
    }
}

fn create_endpoint() -> Result<Endpoint> {
    let mut roots = rustls::RootCertStore::empty();
    let native_certs = rustls_native_certs::load_native_certs();
    for cert in native_certs.certs {
        roots.add(cert).ok();
    }

    let mut crypto = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_no_client_auth();
    crypto.alpn_protocols = vec![ALPN_NB_TX.to_vec()];

    let mut transport = TransportConfig::default();
    transport.max_idle_timeout(Some(quinn::IdleTimeout::try_from(Duration::from_secs(60))?));
    transport.keep_alive_interval(Some(Duration::from_secs(15)));

    let mut client_config = ClientConfig::new(Arc::new(
        quinn::crypto::rustls::QuicClientConfig::try_from(crypto)?,
    ));
    client_config.transport_config(Arc::new(transport));

    let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
    endpoint.set_default_client_config(client_config);

    Ok(endpoint)
}

#[tokio::main]
async fn main() -> Result<()> {
    let api_key = std::env::var("NEXTBLOCK_API_KEY")
        .context("set NEXTBLOCK_API_KEY before running")?;

    let client = NextblockQuicClient::connect("london.nextblock.io:11100", &api_key).await?;

    // Replace this with the signed bytes from your existing Solana flow.
    // For example: let raw_tx = bincode::serialize(&signed_transaction)?;
    let raw_tx: Vec<u8> = vec![0; 32];

    client.send_transaction(&raw_tx).await?;
    println!("transaction queued");

    Ok(())
}
```

## What To Replace

* Swap `london.nextblock.io:11100` for the region closest to you.
* Replace `raw_tx` with the serialized bytes of your signed transaction.
* Read your API key from `NEXTBLOCK_API_KEY` or your own config loader.

## Notes

* Keep the client alive and reuse it for many sends.
* The server expects raw transaction bytes, not base64.
* QUIC does not support the extra gRPC submission flags or atomic bundle submission.
* If you already build transactions with `solana-sdk`, serializing the signed transaction is enough before calling `send_transaction()`.

See [QUIC Transaction Submission](/api/quic-transaction-submission) for the full endpoint list and protocol summary.


# Submit Single Transaction

Submit individual transactions to NextBlock using Rust and the Solana SDK.

If you want to send raw signed transaction bytes over QUIC instead of gRPC, see [QUIC Transaction Submission](/api/examples/rust/quic).

This example shows transaction construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

## Example

<pre class="language-rust"><code class="lang-rust"><strong>use solana_sdk::{
</strong>    instruction::Instruction,
    message::Message,
    pubkey::Pubkey,
    signature::{Keypair, Signature, Signer},
    system_instruction,
    transaction::Transaction,
    hash::Hash,
};
use solana_client::rpc_client::RpcClient;
use rand::seq::SliceRandom;
use std::str::FromStr;

<strong>// NextBlock tip wallets for load balancing
</strong>const NEXTBLOCK_TIP_WALLETS: [&#x26;str; 8] = [
    "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
    "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc", 
    "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE",
    "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
    "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
    "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
    "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
    "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
];

<strong>// Get random tip wallet for load balancing
</strong>fn get_random_nextblock_tip_wallet() -> Result&#x3C;Pubkey, Box&#x3C;dyn std::error::Error>> {
    let mut rng = rand::thread_rng();
    let wallet_str = NEXTBLOCK_TIP_WALLETS.choose(&#x26;mut rng)
        .ok_or("No tip wallets available")?;
    Ok(Pubkey::from_str(wallet_str)?)
}

<strong>// Build transaction with tip and instructions
</strong>fn build_transaction_with_tip(
    payer: &#x26;Keypair,
    recent_blockhash: Hash,
    tip_amount: u64,
    instructions: Vec&#x3C;Instruction>,
) -> Result&#x3C;Transaction, Box&#x3C;dyn std::error::Error>> {
    let tip_wallet = get_random_nextblock_tip_wallet()?;
    
<strong>    // Create tip instruction (should be first)
</strong>    let tip_instruction = system_instruction::transfer(
        &#x26;payer.pubkey(),
        &#x26;tip_wallet,
        tip_amount,
    );
    
<strong>    // Combine tip instruction with user instructions
</strong>    let mut all_instructions = vec![tip_instruction];
    all_instructions.extend(instructions);
    
<strong>    // Create and sign transaction
</strong>    let message = Message::new(&#x26;all_instructions, Some(&#x26;payer.pubkey()));
    let mut transaction = Transaction::new_unsigned(message);
    transaction.sign(&#x26;[payer], recent_blockhash);
    
    Ok(transaction)
}

<strong>// Submit transaction to NextBlock
</strong>async fn submit_single_transaction(
    // nextblock_client: &#x26;mut YourGeneratedApiClient,
    rpc_client: &#x26;RpcClient,
    signer: &#x26;Keypair,
    recipient: Pubkey,
    transfer_amount: u64,
    tip_amount: u64,
) -> Result&#x3C;Signature, Box&#x3C;dyn std::error::Error>> {
<strong>    // Get recent blockhash
</strong>    let recent_blockhash = rpc_client.get_latest_blockhash()?;
    
<strong>    // Create transfer instruction
</strong>    let transfer_instruction = system_instruction::transfer(
        &#x26;signer.pubkey(),
        &#x26;recipient,
        transfer_amount,
    );
    
<strong>    // Build transaction with tip
</strong>    let transaction = build_transaction_with_tip(
        signer,
        recent_blockhash,
        tip_amount,
        vec![transfer_instruction],
    )?;
    
<strong>    // Convert to base64 for submission
</strong>    let serialized_tx = bincode::serialize(&#x26;transaction)?;
    let base64_tx = base64::encode(serialized_tx);
    
<strong>    // Configure submission options
</strong>    let front_running_protection = false;
    let revert_on_fail = false;
    let disable_retries = false;
    let snipe_transaction = false;
    
<strong>    // Submit to NextBlock
</strong>    /* Uncomment when you have the generated API client
    let request = tonic::Request::new(PostSubmitRequest {
        transaction: Some(TransactionMessage {
            content: base64_tx,
        }),
        skip_pre_flight: Some(true),
        snipe_transaction: Some(snipe_transaction),
        front_running_protection: Some(front_running_protection),
        disable_retries: Some(disable_retries),
        revert_on_fail: Some(revert_on_fail),
    });
    
    let response = nextblock_client.post_submit_v2(request).await?;
    let submit_response = response.into_inner();
    
    println!("Transaction submitted successfully!");
    println!("Signature: {}", submit_response.signature);
    println!("UUID: {}", submit_response.uuid);
    
    Ok(Signature::from_str(&#x26;submit_response.signature)?)
    */
    
    println!("Local transaction built successfully: {}", transaction.signatures[0]);
    Ok(transaction.signatures[0])
}

<strong>// Example with dynamic tip calculation
</strong>async fn submit_with_optimal_tip(
    // nextblock_client: &#x26;mut YourGeneratedApiClient,
    rpc_client: &#x26;RpcClient,
    signer: &#x26;Keypair,
    recipient: Pubkey,
    transfer_amount: u64,
) -> Result&#x3C;Signature, Box&#x3C;dyn std::error::Error>> {
<strong>    // Get optimal tip amount (implement tip floor API call)
</strong>    let optimal_tip = get_optimal_tip_amount().await?;
    
    submit_single_transaction(
        // nextblock_client,
        rpc_client,
        signer,
        recipient,
        transfer_amount,
        optimal_tip,
    ).await
}

<strong>// Get optimal tip amount from tip floor API
</strong>async fn get_optimal_tip_amount() -> Result&#x3C;u64, Box&#x3C;dyn std::error::Error>> {
<strong>    // This should call the tip floor API to get current percentiles
</strong>    // For now, return a reasonable default
    Ok(1_000_000) // 0.001 SOL
}
</code></pre>

## Usage Example

<pre class="language-rust"><code class="lang-rust"><strong>#[tokio::main]
</strong>async fn main() -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // Initialize configuration
</strong>    let signer = Keypair::new(); // Use your actual keypair
    let recipient = Pubkey::from_str("&#x3C;recipient-public-key>")?;
    let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
    
<strong>    // Connect to NextBlock (see connection.md)
</strong>    // let config = NextBlockConfig::default();
    // let mut nextblock_client = create_nextblock_client(config).await?;
    
<strong>    // Submit transaction
</strong>    let signature = submit_single_transaction(
        // &#x26;mut nextblock_client,
        &#x26;rpc_client,
        &#x26;signer,
        recipient,
        10_000,     // Transfer 10,000 lamports
        1_000_000,  // Tip 1,000,000 lamports (0.001 SOL)
    ).await?;
    
    println!("Transaction submitted with signature: {}", signature);
    
<strong>    // Alternative: Submit with optimal tip
</strong>    let signature2 = submit_with_optimal_tip(
        // &#x26;mut nextblock_client,
        &#x26;rpc_client,
        &#x26;signer,
        recipient,
        20_000, // Transfer 20,000 lamports
    ).await?;
    
    println!("Optimally tipped transaction: {}", signature2);
    Ok(())
}
</code></pre>

## Advanced Features

### Custom Instruction Building

<pre class="language-rust"><code class="lang-rust"><strong>use solana_sdk::instruction::{AccountMeta, Instruction};
</strong>
<strong>// Build custom program instruction
</strong>fn build_custom_instruction(
    program_id: Pubkey,
    accounts: Vec&#x3C;AccountMeta>,
    data: Vec&#x3C;u8>,
) -> Instruction {
    Instruction {
        program_id,
        accounts,
        data,
    }
}

<strong>// Example: Token transfer instruction
</strong>fn build_token_transfer_instruction(
    source: Pubkey,
    destination: Pubkey,
    authority: Pubkey,
    amount: u64,
) -> Instruction {
    // This is a simplified example - use spl-token crate for real token transfers
    let accounts = vec![
        AccountMeta::new(source, false),
        AccountMeta::new(destination, false),
        AccountMeta::new_readonly(authority, true),
    ];
    
    build_custom_instruction(
        spl_token::id(),
        accounts,
        amount.to_le_bytes().to_vec(),
    )
}
</code></pre>

### Error Handling and Retries

<pre class="language-rust"><code class="lang-rust"><strong>use tokio::time::{sleep, Duration};
</strong>
<strong>// Retry logic with exponential backoff
</strong>async fn submit_with_retry(
    // nextblock_client: &#x26;mut YourGeneratedApiClient,
    rpc_client: &#x26;RpcClient,
    signer: &#x26;Keypair,
    recipient: Pubkey,
    transfer_amount: u64,
    tip_amount: u64,
    max_retries: u32,
) -> Result&#x3C;Signature, Box&#x3C;dyn std::error::Error>> {
    let mut retry_count = 0;
    let mut delay = Duration::from_millis(1000);
    
    loop {
        match submit_single_transaction(
            // nextblock_client,
            rpc_client,
            signer,
            recipient,
            transfer_amount,
            tip_amount,
        ).await {
            Ok(signature) => return Ok(signature),
            Err(e) if retry_count &#x3C; max_retries => {
                println!("Attempt {} failed: {}. Retrying in {:?}...", 
                        retry_count + 1, e, delay);
                sleep(delay).await;
                retry_count += 1;
                delay *= 2; // Exponential backoff
            }
            Err(e) => return Err(e),
        }
    }
}
</code></pre>

## Best Practices

1. **Always include tips**: NextBlock prioritizes transactions with appropriate tips
2. **Use random tip wallets**: Distribute load across multiple tip addresses
3. **Monitor tip floors**: Adjust tips based on current network conditions
4. **Handle errors gracefully**: Implement retry logic for network issues
5. **Validate inputs**: Always validate public keys and amounts before submission
6. **Use appropriate RPC endpoints**: Choose reliable RPC providers
7. **Keep connections alive**: Reuse gRPC connections for better performance


# Submit Batched Transactions

Submit 2-4 transactions as an atomic bundle to NextBlock using Rust. Batched transactions are processed as Jito bundles - either all transactions succeed, or none do.

This example shows bundle construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

## Example

<pre class="language-rust"><code class="lang-rust"><strong>use solana_sdk::{
</strong>    instruction::Instruction,
    message::Message,
    pubkey::Pubkey,
    signature::{Keypair, Signature, Signer},
    system_instruction,
    transaction::Transaction,
    hash::Hash,
};
use solana_client::rpc_client::RpcClient;
use rand::seq::SliceRandom;
use std::str::FromStr;

<strong>// NextBlock tip wallets
</strong>const NEXTBLOCK_TIP_WALLETS: [&#x26;str; 8] = [
    "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
    "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc",
    "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE", 
    "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
    "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
    "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
    "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
    "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
];

<strong>// Transaction builder for bundle
</strong>struct TransactionBundle {
    transactions: Vec&#x3C;Transaction>,
}

impl TransactionBundle {
    fn new() -> Self {
        Self {
            transactions: Vec::new(),
        }
    }
    
<strong>    // Add transaction to bundle
</strong>    fn add_transaction(&#x26;mut self, transaction: Transaction) -> Self {
        self.transactions.push(transaction);
        self
    }
    
<strong>    // Get transactions as base64 strings
</strong>    fn to_base64_transactions(&#x26;self) -> Result&#x3C;Vec&#x3C;String>, Box&#x3C;dyn std::error::Error>> {
        self.transactions
            .iter()
            .map(|tx| {
                let serialized = bincode::serialize(tx)?;
                Ok(base64::encode(serialized))
            })
            .collect()
    }
}

<strong>// Build single transaction with tip
</strong>fn build_transaction_with_tip(
    payer: &#x26;Keypair,
    recent_blockhash: Hash,
    tip_amount: u64,
    instructions: Vec&#x3C;Instruction>,
) -> Result&#x3C;Transaction, Box&#x3C;dyn std::error::Error>> {
    let mut rng = rand::thread_rng();
    let tip_wallet_str = NEXTBLOCK_TIP_WALLETS.choose(&#x26;mut rng)
        .ok_or("No tip wallets available")?;
    let tip_wallet = Pubkey::from_str(tip_wallet_str)?;
    
<strong>    // Create tip instruction (should be first)
</strong>    let tip_instruction = system_instruction::transfer(
        &#x26;payer.pubkey(),
        &#x26;tip_wallet,
        tip_amount,
    );
    
<strong>    // Combine instructions
</strong>    let mut all_instructions = vec![tip_instruction];
    all_instructions.extend(instructions);
    
<strong>    // Build and sign transaction
</strong>    let message = Message::new(&#x26;all_instructions, Some(&#x26;payer.pubkey()));
    let mut transaction = Transaction::new_unsigned(message);
    transaction.sign(&#x26;[payer], recent_blockhash);
    
    Ok(transaction)
}

<strong>// Submit batched transactions
</strong>async fn submit_batched_transactions(
    // nextblock_client: &#x26;mut YourGeneratedApiClient,
    rpc_client: &#x26;RpcClient,
    signer: &#x26;Keypair,
) -> Result&#x3C;String, Box&#x3C;dyn std::error::Error>> {
<strong>    // Get recent blockhash (same for all transactions in bundle)
</strong>    let recent_blockhash = rpc_client.get_latest_blockhash()?;
    
<strong>    // Build transaction bundle
</strong>    let mut bundle = TransactionBundle::new();
    
<strong>    // Transaction 1: Setup transaction
</strong>    let setup_instructions = vec![
        system_instruction::transfer(
            &#x26;signer.pubkey(),
            &#x26;Pubkey::from_str("&#x3C;recipient1-public-key>")?,
            100_000, // 0.0001 SOL
        ),
    ];
    
    let setup_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        500_000, // 0.0005 SOL tip
        setup_instructions,
    )?;
    bundle = bundle.add_transaction(setup_tx);
    
<strong>    // Transaction 2: Main operation
</strong>    let main_instructions = vec![
        system_instruction::transfer(
            &#x26;signer.pubkey(),
            &#x26;Pubkey::from_str("&#x3C;recipient2-public-key>")?,
            200_000, // 0.0002 SOL
        ),
    ];
    
    let main_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        1_000_000, // 0.001 SOL tip (higher for main transaction)
        main_instructions,
    )?;
    bundle = bundle.add_transaction(main_tx);
    
<strong>    // Transaction 3: Cleanup transaction
</strong>    let cleanup_instructions = vec![
        system_instruction::transfer(
            &#x26;signer.pubkey(),
            &#x26;Pubkey::from_str("&#x3C;recipient3-public-key>")?,
            50_000, // 0.00005 SOL
        ),
    ];
    
    let cleanup_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        500_000, // 0.0005 SOL tip
        cleanup_instructions,
    )?;
    bundle = bundle.add_transaction(cleanup_tx);
    
<strong>    // Convert to base64 for submission
</strong>    let base64_transactions = bundle.to_base64_transactions()?;
    
<strong>    // Print transaction signatures for tracking
</strong>    for (i, tx) in bundle.transactions.iter().enumerate() {
        println!("Transaction {} signature: {}", i + 1, tx.signatures[0]);
    }
    
<strong>    // Submit bundle to NextBlock
</strong>    /* Uncomment when you have the generated API client
    let entries: Vec&#x3C;PostSubmitRequestEntry> = base64_transactions
        .into_iter()
        .map(|tx_base64| PostSubmitRequestEntry {
            transaction: Some(TransactionMessage {
                content: tx_base64,
            }),
        })
        .collect();
    
    let request = tonic::Request::new(PostSubmitBatchRequest {
        entries,
    });
    
    let response = nextblock_client.post_submit_batch_v2(request).await?;
    let submit_response = response.into_inner();
    
    println!("Batch submitted successfully!");
    println!("Bundle signature: {}", submit_response.signature);
    
    Ok(submit_response.signature)
    */
    
    println!("Local bundle prepared with {} transactions.", bundle.transactions.len());
    Ok("".to_string())
}
</code></pre>

## Arbitrage Bundle Example

<pre class="language-rust"><code class="lang-rust"><strong>// Example: Build arbitrage bundle
</strong>async fn build_arbitrage_bundle(
    // nextblock_client: &#x26;mut YourGeneratedApiClient,
    rpc_client: &#x26;RpcClient,
    signer: &#x26;Keypair,
    dex_a_address: Pubkey,
    dex_b_address: Pubkey,
    trade_amount: u64,
) -> Result&#x3C;String, Box&#x3C;dyn std::error::Error>> {
    let recent_blockhash = rpc_client.get_latest_blockhash()?;
    let mut bundle = TransactionBundle::new();
    
<strong>    // Transaction 1: Buy on DEX A
</strong>    let buy_instructions = vec![
<strong>        // Add your DEX-specific buy instructions here
</strong>        system_instruction::transfer(
            &#x26;signer.pubkey(),
            &#x26;dex_a_address,
            trade_amount,
        ),
    ];
    
    let buy_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        2_000_000, // Higher tip for arbitrage
        buy_instructions,
    )?;
    bundle = bundle.add_transaction(buy_tx);
    
<strong>    // Transaction 2: Sell on DEX B
</strong>    let sell_instructions = vec![
<strong>        // Add your DEX-specific sell instructions here
</strong>        system_instruction::transfer(
            &#x26;signer.pubkey(),
            &#x26;dex_b_address,
            trade_amount,
        ),
    ];
    
    let sell_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        2_000_000, // Higher tip for arbitrage
        sell_instructions,
    )?;
    bundle = bundle.add_transaction(sell_tx);
    
    println!("Arbitrage bundle prepared with {} transactions", bundle.transactions.len());
    
<strong>    // Submit the bundle
</strong>    // Implementation similar to submit_batched_transactions
    Ok("".to_string())
}
</code></pre>

## Complex DeFi Bundle Example

<pre class="language-rust"><code class="lang-rust"><strong>// Example: Complex DeFi operation bundle
</strong>async fn build_defi_operation_bundle(
    // nextblock_client: &#x26;mut YourGeneratedApiClient,
    rpc_client: &#x26;RpcClient,
    signer: &#x26;Keypair,
) -> Result&#x3C;String, Box&#x3C;dyn std::error::Error>> {
    let recent_blockhash = rpc_client.get_latest_blockhash()?;
    let mut bundle = TransactionBundle::new();
    
<strong>    // Transaction 1: Create token accounts
</strong>    let account_creation_instructions = vec![
<strong>        // Add token account creation instructions
</strong>        system_instruction::create_account(
            &#x26;signer.pubkey(),
            &#x26;Keypair::new().pubkey(), // New account
            1_000_000, // Rent exemption
            165,       // Token account space
            &#x26;spl_token::id(),
        ),
    ];
    
    let account_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        500_000,
        account_creation_instructions,
    )?;
    bundle = bundle.add_transaction(account_tx);
    
<strong>    // Transaction 2: Execute swap
</strong>    let swap_instructions = vec![
<strong>        // Add swap instructions (Jupiter, Raydium, etc.)
</strong>        system_instruction::transfer(
            &#x26;signer.pubkey(),
            &#x26;Pubkey::from_str("&#x3C;swap-program-address>")?,
            0, // No SOL transfer for swap
        ),
    ];
    
    let swap_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        1_500_000, // Higher tip for main operation
        swap_instructions,
    )?;
    bundle = bundle.add_transaction(swap_tx);
    
<strong>    // Transaction 3: Stake or provide liquidity
</strong>    let stake_instructions = vec![
<strong>        // Add staking/liquidity instructions
</strong>        system_instruction::transfer(
            &#x26;signer.pubkey(),
            &#x26;Pubkey::from_str("&#x3C;stake-pool-address>")?,
            0,
        ),
    ];
    
    let stake_tx = build_transaction_with_tip(
        signer,
        recent_blockhash,
        750_000,
        stake_instructions,
    )?;
    bundle = bundle.add_transaction(stake_tx);
    
    println!("DeFi operation bundle prepared with {} transactions", bundle.transactions.len());
    Ok("".to_string())
}
</code></pre>

## Usage Example

<pre class="language-rust"><code class="lang-rust"><strong>#[tokio::main]
</strong>async fn main() -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // Initialize configuration
</strong>    let signer = Keypair::new(); // Use your actual keypair
    let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
    
<strong>    // Connect to NextBlock (see connection.md)
</strong>    // let config = NextBlockConfig::default();
    // let mut nextblock_client = create_nextblock_client(config).await?;
    
<strong>    // Submit basic batch
</strong>    let bundle_signature = submit_batched_transactions(
        // &#x26;mut nextblock_client,
        &#x26;rpc_client,
        &#x26;signer,
    ).await?;
    
    println!("Batch submitted with signature: {}", bundle_signature);
    
<strong>    // Submit arbitrage bundle
</strong>    let arb_signature = build_arbitrage_bundle(
        // &#x26;mut nextblock_client,
        &#x26;rpc_client,
        &#x26;signer,
        Pubkey::from_str("&#x3C;dex-a-address>")?,
        Pubkey::from_str("&#x3C;dex-b-address>")?,
        1_000_000, // 0.001 SOL
    ).await?;
    
    println!("Arbitrage bundle: {}", arb_signature);
    
<strong>    // Submit complex DeFi bundle
</strong>    let defi_signature = build_defi_operation_bundle(
        // &#x26;mut nextblock_client,
        &#x26;rpc_client,
        &#x26;signer,
    ).await?;
    
    println!("DeFi bundle: {}", defi_signature);
    
    Ok(())
}
</code></pre>

## Bundle Best Practices

### Size and Composition

* **Bundle size**: 2-4 transactions (optimal: 2-3)
* **Transaction order**: Setup → Main operations → Cleanup
* **Compute limits**: Stay within bundle compute unit limits

### Tip Strategy

* **Progressive tipping**: Higher tips for more critical transactions
* **Bundle coherence**: All transactions should have meaningful tips
* **Market conditions**: Adjust tips based on network congestion

### Error Handling

<pre class="language-rust"><code class="lang-rust"><strong>// Validate bundle before submission
</strong>fn validate_bundle(bundle: &#x26;TransactionBundle) -> Result&#x3C;(), &#x26;'static str> {
    if bundle.transactions.is_empty() {
        return Err("Bundle cannot be empty");
    }
    
    if bundle.transactions.len() > 4 {
        return Err("Bundle cannot contain more than 4 transactions");
    }
    
    if bundle.transactions.len() &#x3C; 2 {
        return Err("Bundle must contain at least 2 transactions");
    }
    
<strong>    // Check for signature duplicates
</strong>    let mut signatures = std::collections::HashSet::new();
    for tx in &#x26;bundle.transactions {
        if !signatures.insert(tx.signatures[0]) {
            return Err("Duplicate transaction signatures in bundle");
        }
    }
    
    Ok(())
}
</code></pre>

## Common Use Cases

1. **Arbitrage**: Multi-DEX price differences
2. **DeFi operations**: Account setup → Trade → Stake
3. **NFT operations**: Create → Mint → Transfer → List
4. **Token operations**: Create mint → Create accounts → Transfer
5. **Liquidations**: Detect → Execute → Collect rewards

## Bundle Failure Prevention

* **Account validation**: Ensure all accounts exist and have sufficient balance
* **Instruction compatibility**: Avoid conflicting program interactions
* **Blockhash freshness**: Use recent blockhashes (valid \~60 seconds)
* **Compute budget**: Monitor total compute units across all transactions


# Tip Floor Stream

Stream real-time tip floor data from NextBlock to optimize your transaction tips dynamically.

## Streaming Tip Floor Data

<pre class="language-rust"><code class="lang-rust"><strong>use tokio_stream::StreamExt;
</strong>use std::time::Duration;
use serde::{Deserialize, Serialize};

<strong>// Tip floor response structure
</strong>#[derive(Debug, Deserialize, Serialize)]
pub struct TipFloorData {
    pub time: String,
    pub landed_tips_25th_percentile: f64,
    pub landed_tips_50th_percentile: f64,
    pub landed_tips_75th_percentile: f64,
    pub landed_tips_95th_percentile: f64,
    pub landed_tips_99th_percentile: f64,
    pub ema_landed_tips_50th_percentile: f64,
}

<strong>// Stream tip floor updates
</strong>async fn stream_tip_floor(
    // nextblock_client: &#x26;mut YourGeneratedApiClient,
    update_frequency: String,
) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
    println!("Starting tip floor stream with frequency: {}", update_frequency);
    
<strong>    // Create streaming request
</strong>    /* Uncomment when you have the generated API client
    let request = tonic::Request::new(TipFloorStreamRequest {
        update_frequency,
    });
    
    let mut stream = nextblock_client
        .stream_tip_floor(request)
        .await?
        .into_inner();
    
    println!("Streaming tip floor data:");
    
    while let Some(tip_floor_response) = stream.next().await {
        match tip_floor_response {
            Ok(tip_floor) => {
                println!("Received tip floor update:");
                println!("  Time: {}", tip_floor.time);
                println!("  25th percentile: {:.6} SOL", tip_floor.landed_tips_25th_percentile);
                println!("  50th percentile: {:.6} SOL", tip_floor.landed_tips_50th_percentile);
                println!("  75th percentile: {:.6} SOL", tip_floor.landed_tips_75th_percentile);
                println!("  95th percentile: {:.6} SOL", tip_floor.landed_tips_95th_percentile);
                println!("  99th percentile: {:.6} SOL", tip_floor.landed_tips_99th_percentile);
                println!("  EMA 50th percentile: {:.6} SOL", tip_floor.ema_landed_tips_50th_percentile);
                println!("  ---");
                
                // Process the tip floor data
                process_tip_floor_update(&#x26;tip_floor).await?;
            }
            Err(e) => {
                eprintln!("Stream error: {}", e);
                // Implement reconnection logic here
                break;
            }
        }
    }
    */
    
<strong>    // Mock streaming for demonstration
</strong>    println!("Mock tip floor streaming started...");
    let mut interval = tokio::time::interval(Duration::from_secs(60));
    
    loop {
        interval.tick().await;
        let mock_tip_floor = TipFloorData {
            time: chrono::Utc::now().to_rfc3339(),
            landed_tips_25th_percentile: 0.0011,
            landed_tips_50th_percentile: 0.005000001,
            landed_tips_75th_percentile: 0.01555,
            landed_tips_95th_percentile: 0.09339195639999975,
            landed_tips_99th_percentile: 0.4846427910400001,
            ema_landed_tips_50th_percentile: 0.005989477267191758,
        };
        
        println!("Mock tip floor update: {:#?}", mock_tip_floor);
        process_tip_floor_update(&#x26;mock_tip_floor).await?;
    }
}

<strong>// Process tip floor updates
</strong>async fn process_tip_floor_update(
    tip_floor: &#x26;TipFloorData,
) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // Update global tip strategy
</strong>    update_tip_strategy(tip_floor).await?;
    
<strong>    // Optionally store historical data
</strong>    store_tip_floor_data(tip_floor).await?;
    
<strong>    // Trigger any pending transactions with updated tips
</strong>    trigger_pending_transactions().await?;
    
    Ok(())
}
</code></pre>

## Dynamic Tip Calculation

<pre class="language-rust"><code class="lang-rust"><strong>use std::sync::Arc;
</strong>use tokio::sync::RwLock;

<strong>// Global tip strategy state
</strong>#[derive(Debug, Clone)]
pub struct TipStrategy {
    pub conservative_tip: u64,  // 25th percentile
    pub normal_tip: u64,        // 50th percentile  
    pub aggressive_tip: u64,    // 75th percentile
    pub priority_tip: u64,      // 95th percentile
    pub last_updated: chrono::DateTime&#x3C;chrono::Utc>,
}

impl Default for TipStrategy {
    fn default() -> Self {
        Self {
            conservative_tip: 500_000,   // 0.0005 SOL
            normal_tip: 1_000_000,      // 0.001 SOL
            aggressive_tip: 2_000_000,  // 0.002 SOL
            priority_tip: 5_000_000,    // 0.005 SOL
            last_updated: chrono::Utc::now(),
        }
    }
}

<strong>// Global tip strategy instance
</strong>static TIP_STRATEGY: once_cell::sync::Lazy&#x3C;Arc&#x3C;RwLock&#x3C;TipStrategy>>> = 
    once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(TipStrategy::default())));

<strong>// Convert SOL to lamports
</strong>fn sol_to_lamports(sol: f64) -> u64 {
    (sol * 1_000_000_000.0) as u64
}

<strong>// Update tip strategy based on tip floor data
</strong>async fn update_tip_strategy(
    tip_floor: &#x26;TipFloorData,
) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
    let mut strategy = TIP_STRATEGY.write().await;
    
    strategy.conservative_tip = sol_to_lamports(tip_floor.landed_tips_25th_percentile);
    strategy.normal_tip = sol_to_lamports(tip_floor.landed_tips_50th_percentile);
    strategy.aggressive_tip = sol_to_lamports(tip_floor.landed_tips_75th_percentile);
    strategy.priority_tip = sol_to_lamports(tip_floor.landed_tips_95th_percentile);
    strategy.last_updated = chrono::Utc::now();
    
    println!("Updated tip strategy:");
    println!("  Conservative: {} lamports ({:.6} SOL)", 
             strategy.conservative_tip, tip_floor.landed_tips_25th_percentile);
    println!("  Normal: {} lamports ({:.6} SOL)", 
             strategy.normal_tip, tip_floor.landed_tips_50th_percentile);
    println!("  Aggressive: {} lamports ({:.6} SOL)", 
             strategy.aggressive_tip, tip_floor.landed_tips_75th_percentile);
    println!("  Priority: {} lamports ({:.6} SOL)", 
             strategy.priority_tip, tip_floor.landed_tips_95th_percentile);
    
    Ok(())
}

<strong>// Get optimal tip for transaction priority
</strong>pub async fn get_optimal_tip(priority: TipPriority) -> u64 {
    let strategy = TIP_STRATEGY.read().await;
    
    match priority {
        TipPriority::Conservative => strategy.conservative_tip,
        TipPriority::Normal => strategy.normal_tip,
        TipPriority::Aggressive => strategy.aggressive_tip,
        TipPriority::Priority => strategy.priority_tip,
    }
}

<strong>// Tip priority levels
</strong>#[derive(Debug, Clone, Copy)]
pub enum TipPriority {
    Conservative, // 25th percentile - cheapest option
    Normal,       // 50th percentile - balanced option
    Aggressive,   // 75th percentile - faster execution
    Priority,     // 95th percentile - highest priority
}
</code></pre>

## Advanced Tip Management

<pre class="language-rust"><code class="lang-rust"><strong>use std::collections::VecDeque;
</strong>
<strong>// Historical tip data for trend analysis
</strong>#[derive(Debug)]
pub struct TipHistory {
    data: VecDeque&#x3C;TipFloorData>,
    max_size: usize,
}

impl TipHistory {
    pub fn new(max_size: usize) -> Self {
        Self {
            data: VecDeque::with_capacity(max_size),
            max_size,
        }
    }
    
    pub fn add(&#x26;mut self, tip_floor: TipFloorData) {
        if self.data.len() >= self.max_size {
            self.data.pop_front();
        }
        self.data.push_back(tip_floor);
    }
    
<strong>    // Calculate trend (increasing/decreasing tips)
</strong>    pub fn calculate_trend(&#x26;self) -> f64 {
        if self.data.len() &#x3C; 2 {
            return 0.0;
        }
        
        let recent = &#x26;self.data[self.data.len() - 1];
        let older = &#x26;self.data[self.data.len() - 2];
        
        recent.landed_tips_50th_percentile - older.landed_tips_50th_percentile
    }
    
<strong>    // Get average tip over time window
</strong>    pub fn get_average_tip(&#x26;self, percentile: &#x26;str) -> f64 {
        if self.data.is_empty() {
            return 0.0;
        }
        
        let sum: f64 = self.data.iter().map(|d| {
            match percentile {
                "25th" => d.landed_tips_25th_percentile,
                "50th" => d.landed_tips_50th_percentile,
                "75th" => d.landed_tips_75th_percentile,
                "95th" => d.landed_tips_95th_percentile,
                _ => d.landed_tips_50th_percentile,
            }
        }).sum();
        
        sum / self.data.len() as f64
    }
}

<strong>// Smart tip calculation with trend analysis
</strong>pub async fn get_smart_tip(
    base_priority: TipPriority,
    tip_history: &#x26;TipHistory,
) -> u64 {
    let base_tip = get_optimal_tip(base_priority).await;
    let trend = tip_history.calculate_trend();
    
<strong>    // Adjust tip based on trend
</strong>    let adjustment_factor = if trend > 0.001 {
        1.2 // Tips are increasing, be more aggressive
    } else if trend &#x3C; -0.001 {
        0.9 // Tips are decreasing, can be more conservative
    } else {
        1.0 // No significant trend
    };
    
    ((base_tip as f64) * adjustment_factor) as u64
}
</code></pre>

## Store Historical Data

<pre class="language-rust"><code class="lang-rust"><strong>// Store tip floor data for analysis
</strong>async fn store_tip_floor_data(
    tip_floor: &#x26;TipFloorData,
) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // Example: Store to file (in production, use a database)
</strong>    let data_dir = std::path::Path::new("./tip_data");
    if !data_dir.exists() {
        tokio::fs::create_dir_all(data_dir).await?;
    }
    
    let filename = format!("tip_floor_{}.json", 
                          chrono::Utc::now().format("%Y%m%d"));
    let filepath = data_dir.join(filename);
    
<strong>    // Append to daily file
</strong>    let json_line = format!("{}\n", serde_json::to_string(tip_floor)?);
    tokio::fs::write(&#x26;filepath, json_line).await?;
    
    println!("Stored tip floor data to {:?}", filepath);
    Ok(())
}

<strong>// Trigger pending transactions with updated tips
</strong>async fn trigger_pending_transactions() -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // This would check for any pending transactions that are waiting
</strong>    // for better tip conditions and submit them with updated tips
    println!("Checking for pending transactions to trigger...");
    
<strong>    // Example: Check if any transactions are waiting for lower tips
</strong>    // and submit them now if conditions are favorable
    
    Ok(())
}
</code></pre>

## Usage Example

<pre class="language-rust"><code class="lang-rust"><strong>#[tokio::main]
</strong>async fn main() -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // Connect to NextBlock (see connection.md)
</strong>    // let config = NextBlockConfig::default();
    // let mut nextblock_client = create_nextblock_client(config).await?;
    
<strong>    // Start tip floor streaming in background
</strong>    let stream_handle = tokio::spawn(async move {
        if let Err(e) = stream_tip_floor(
            // &#x26;mut nextblock_client,
            "1m".to_string(), // Update every minute
        ).await {
            eprintln!("Tip floor streaming error: {}", e);
        }
    });
    
<strong>    // Example: Use dynamic tips in transaction submission
</strong>    tokio::time::sleep(Duration::from_secs(5)).await; // Wait for initial data
    
    let conservative_tip = get_optimal_tip(TipPriority::Conservative).await;
    let normal_tip = get_optimal_tip(TipPriority::Normal).await;
    let aggressive_tip = get_optimal_tip(TipPriority::Aggressive).await;
    
    println!("Current optimal tips:");
    println!("  Conservative: {} lamports", conservative_tip);
    println!("  Normal: {} lamports", normal_tip);
    println!("  Aggressive: {} lamports", aggressive_tip);
    
<strong>    // Use these tips in your transaction submissions
</strong>    // submit_transaction_with_tip(normal_tip).await?;
    
<strong>    // Keep the stream running
</strong>    stream_handle.await??;
    
    Ok(())
}
</code></pre>

## Best Practices

1. **Update frequency**: Use 1-5 minute intervals for most applications
2. **Trend analysis**: Consider tip trends when calculating optimal amounts
3. **Fallback values**: Always have default tip values in case streaming fails
4. **Historical data**: Store tip floor data for analysis and optimization
5. **Priority levels**: Use different tip strategies for different transaction types
6. **Error handling**: Implement reconnection logic for stream interruptions
7. **Resource management**: Limit historical data storage to prevent memory issues


# Keepalive

Maintain persistent gRPC connections to NextBlock for optimal performance using Rust's Tonic client.

## Basic Keepalive Implementation

<pre class="language-rust"><code class="lang-rust"><strong>use std::time::Duration;
</strong>use tokio::time::{interval, sleep};
use tonic::Request;

<strong>// Send periodic ping to keep connection alive
</strong>async fn start_keepalive_task(
    // mut nextblock_client: YourGeneratedApiClient,
    ping_interval: Duration,
) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
    let mut interval_timer = interval(ping_interval);
    
    println!("Starting keepalive task with interval: {:?}", ping_interval);
    
    loop {
        interval_timer.tick().await;
        
<strong>        // Send ping request
</strong>        /* Uncomment when you have the generated API client
        match nextblock_client.ping(Request::new(())).await {
            Ok(_) => {
                println!("Keepalive ping successful at {}", 
                        chrono::Utc::now().format("%H:%M:%S"));
            }
            Err(e) => {
                eprintln!("Keepalive ping failed: {}", e);
                // Optionally implement reconnection logic here
                break;
            }
        }
        */
        
<strong>        // Mock ping for demonstration
</strong>        println!("Mock keepalive ping sent at {}", 
                chrono::Utc::now().format("%H:%M:%S"));
    }
    
    Ok(())
}
</code></pre>

## Advanced Keepalive with Connection Management

<pre class="language-rust"><code class="lang-rust"><strong>use std::sync::Arc;
</strong>use tokio::sync::RwLock;
use tonic::transport::Channel;

<strong>// Connection health tracker
</strong>#[derive(Debug, Clone)]
pub struct ConnectionHealth {
    pub is_healthy: bool,
    pub last_successful_ping: chrono::DateTime&#x3C;chrono::Utc>,
    pub consecutive_failures: u32,
    pub total_pings_sent: u64,
    pub total_pings_successful: u64,
}

impl Default for ConnectionHealth {
    fn default() -> Self {
        Self {
            is_healthy: true,
            last_successful_ping: chrono::Utc::now(),
            consecutive_failures: 0,
            total_pings_sent: 0,
            total_pings_successful: 0,
        }
    }
}

<strong>// Advanced keepalive manager
</strong>pub struct KeepaliveManager {
    // client: Arc&#x3C;RwLock&#x3C;YourGeneratedApiClient>>,
    health: Arc&#x3C;RwLock&#x3C;ConnectionHealth>>,
    config: KeepaliveConfig,
}

#[derive(Debug, Clone)]
pub struct KeepaliveConfig {
    pub ping_interval: Duration,
    pub max_consecutive_failures: u32,
    pub reconnect_delay: Duration,
    pub health_check_enabled: bool,
}

impl Default for KeepaliveConfig {
    fn default() -> Self {
        Self {
            ping_interval: Duration::from_secs(60),
            max_consecutive_failures: 3,
            reconnect_delay: Duration::from_secs(5),
            health_check_enabled: true,
        }
    }
}

impl KeepaliveManager {
    pub fn new(
        // client: YourGeneratedApiClient,
        config: KeepaliveConfig,
    ) -> Self {
        Self {
            // client: Arc::new(RwLock::new(client)),
            health: Arc::new(RwLock::new(ConnectionHealth::default())),
            config,
        }
    }
    
<strong>    // Start keepalive task
</strong>    pub async fn start(&#x26;self) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
        let health = Arc::clone(&#x26;self.health);
        // let client = Arc::clone(&#x26;self.client);
        let config = self.config.clone();
        
        tokio::spawn(async move {
            let mut interval_timer = interval(config.ping_interval);
            
            loop {
                interval_timer.tick().await;
                
<strong>                // Send ping and update health
</strong>                Self::send_ping_and_update_health(
                    // Arc::clone(&#x26;client),
                    Arc::clone(&#x26;health),
                    &#x26;config,
                ).await;
            }
        });
        
        Ok(())
    }
    
<strong>    // Send ping and update connection health
</strong>    async fn send_ping_and_update_health(
        // client: Arc&#x3C;RwLock&#x3C;YourGeneratedApiClient>>,
        health: Arc&#x3C;RwLock&#x3C;ConnectionHealth>>,
        config: &#x26;KeepaliveConfig,
    ) {
        let ping_start = std::time::Instant::now();
        
<strong>        // Update ping attempt counter
</strong>        {
            let mut health_guard = health.write().await;
            health_guard.total_pings_sent += 1;
        }
        
<strong>        // Send ping
</strong>        /* Uncomment when you have the generated API client
        let ping_result = {
            let mut client_guard = client.write().await;
            client_guard.ping(Request::new(())).await
        };
        
        match ping_result {
            Ok(_) => {
                let ping_duration = ping_start.elapsed();
                let mut health_guard = health.write().await;
                
                health_guard.is_healthy = true;
                health_guard.last_successful_ping = chrono::Utc::now();
                health_guard.consecutive_failures = 0;
                health_guard.total_pings_successful += 1;
                
                println!("Keepalive ping successful ({}ms) - Health: {:.1}%", 
                        ping_duration.as_millis(),
                        (health_guard.total_pings_successful as f64 / 
                         health_guard.total_pings_sent as f64) * 100.0);
            }
            Err(e) => {
                let mut health_guard = health.write().await;
                health_guard.consecutive_failures += 1;
                
                if health_guard.consecutive_failures >= config.max_consecutive_failures {
                    health_guard.is_healthy = false;
                    eprintln!("Connection marked as unhealthy after {} consecutive failures", 
                             health_guard.consecutive_failures);
                }
                
                eprintln!("Keepalive ping failed (attempt {}): {}", 
                         health_guard.consecutive_failures, e);
            }
        }
        */
        
<strong>        // Mock ping result for demonstration
</strong>        let ping_duration = ping_start.elapsed();
        let mut health_guard = health.write().await;
        health_guard.is_healthy = true;
        health_guard.last_successful_ping = chrono::Utc::now();
        health_guard.consecutive_failures = 0;
        health_guard.total_pings_successful += 1;
        
        println!("Mock keepalive ping successful ({}ms) - Health: {:.1}%", 
                ping_duration.as_millis(),
                (health_guard.total_pings_successful as f64 / 
                 health_guard.total_pings_sent as f64) * 100.0);
    }
    
<strong>    // Get connection health status
</strong>    pub async fn get_health(&#x26;self) -> ConnectionHealth {
        self.health.read().await.clone()
    }
    
<strong>    // Check if connection is healthy
</strong>    pub async fn is_healthy(&#x26;self) -> bool {
        self.health.read().await.is_healthy
    }
}
</code></pre>

## Connection Recovery

<pre class="language-rust"><code class="lang-rust"><strong>// Automatic connection recovery
</strong>pub struct ConnectionManager {
    config: NextBlockConfig,
    keepalive_manager: Option&#x3C;KeepaliveManager>,
    // client: Option&#x3C;YourGeneratedApiClient>,
}

impl ConnectionManager {
    pub fn new(config: NextBlockConfig) -> Self {
        Self {
            config,
            keepalive_manager: None,
            // client: None,
        }
    }
    
<strong>    // Establish connection with automatic recovery
</strong>    pub async fn connect_with_recovery(&#x26;mut self) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
        loop {
            match self.attempt_connection().await {
                Ok(()) => {
                    println!("Successfully connected to NextBlock");
                    
<strong>                    // Start keepalive
</strong>                    self.start_keepalive().await?;
                    
<strong>                    // Monitor connection health
</strong>                    self.monitor_connection_health().await?;
                    break;
                }
                Err(e) => {
                    eprintln!("Connection failed: {}. Retrying in 5 seconds...", e);
                    sleep(Duration::from_secs(5)).await;
                }
            }
        }
        
        Ok(())
    }
    
<strong>    // Attempt to establish connection
</strong>    async fn attempt_connection(&#x26;mut self) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
        // let client = create_nextblock_client(self.config.clone()).await?;
        // self.client = Some(client);
        println!("Mock connection established");
        Ok(())
    }
    
<strong>    // Start keepalive task
</strong>    async fn start_keepalive(&#x26;mut self) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
        let keepalive_config = KeepaliveConfig::default();
        
        // let keepalive_manager = KeepaliveManager::new(
        //     self.client.as_ref().unwrap().clone(),
        //     keepalive_config,
        // );
        
        // keepalive_manager.start().await?;
        // self.keepalive_manager = Some(keepalive_manager);
        
        println!("Keepalive task started");
        Ok(())
    }
    
<strong>    // Monitor connection health and trigger recovery
</strong>    async fn monitor_connection_health(&#x26;self) -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
        let mut health_check_interval = interval(Duration::from_secs(30));
        
        loop {
            health_check_interval.tick().await;
            
            if let Some(ref keepalive_manager) = self.keepalive_manager {
                if !keepalive_manager.is_healthy().await {
                    eprintln!("Connection unhealthy, attempting recovery...");
                    // Trigger reconnection logic
                    return Err("Connection lost".into());
                }
            }
        }
    }
}
</code></pre>

## Usage Example

<pre class="language-rust"><code class="lang-rust"><strong>#[tokio::main]
</strong>async fn main() -> Result&#x3C;(), Box&#x3C;dyn std::error::Error>> {
<strong>    // Basic keepalive usage
</strong>    // let config = NextBlockConfig::default();
    // let client = create_nextblock_client(config).await?;
    
<strong>    // Start simple keepalive task
</strong>    let keepalive_handle = tokio::spawn(async move {
        start_keepalive_task(
            // client,
            Duration::from_secs(60), // Ping every minute
        ).await
    });
    
<strong>    // Advanced usage with connection management
</strong>    let config = NextBlockConfig::default();
    let connection_manager = ConnectionManager::new(config);
    
    let manager_handle = tokio::spawn(async move {
        if let Err(e) = connection_manager.connect_with_recovery().await {
            eprintln!("Connection manager failed: {}", e);
        }
    });
    
<strong>    // Your main application logic here
</strong>    println!("Application running with keepalive...");
    
<strong>    // Wait for tasks
</strong>    tokio::select! {
        result = keepalive_handle => {
            if let Err(e) = result? {
                eprintln!("Keepalive task failed: {}", e);
            }
        }
        result = manager_handle => {
            if let Err(e) = result {
                eprintln!("Connection manager task failed: {}", e);
            }
        }
    }
    
    Ok(())
}
</code></pre>

## Best Practices

1. **Ping frequency**: Use 60-second intervals for most applications
2. **Health monitoring**: Track connection health and implement recovery
3. **Error handling**: Handle ping failures gracefully with exponential backoff
4. **Resource cleanup**: Properly close connections on shutdown
5. **Logging**: Log keepalive status for debugging and monitoring
6. **Timeouts**: Set appropriate timeouts for ping requests
7. **Reconnection logic**: Implement automatic reconnection on connection loss


# Python

Complete Python examples for integrating with NextBlock's gRPC API and QUIC transaction submission using grpcio and solders/solana-py.

## Overview

These examples demonstrate how to:

* Establish secure gRPC connections with authentication
* Submit raw transaction bytes over QUIC for lower submission overhead
* Submit single and batched transactions with proper tipping
* Stream real-time tip floor data for dynamic tip optimization
* Maintain persistent connections with keepalive mechanisms

## Prerequisites

Install the required dependencies:

```bash
pip install grpcio grpcio-tools
pip install solders solana
pip install asyncio aiogrpc
pip install base58 base64
pip install requests

# Generate Python gRPC client from nextblock-proto
# See https://github.com/nextblock-ag/nextblock-proto for instructions
```

## Examples

### Core Examples

* [Connection](/api/examples/python/connection) - Establish gRPC connections with authentication
* [QUIC Transaction Submission](/api/examples/python/quic) - Send raw signed transaction bytes over QUIC
* [Submit Single Transaction](/api/examples/python/submit-single-transactions) - Send individual transactions with tips
* [Submit Batched Transactions](/api/examples/python/submit-batched-transactions) - Send atomic transaction bundles
* [Tip Floor Stream](/api/examples/python/tip-floor-stream) - Stream real-time tip floor data
* [Keepalive](/api/examples/python/keepalive) - Maintain persistent connections

## Quick Start

1. **Generate gRPC client** from [nextblock-proto](https://github.com/nextblock-ag/nextblock-proto)
2. **Install dependencies** using pip
3. **Set up environment** with API key and endpoint
4. **Start with connection example** to establish authenticated gRPC connection
5. **Use tip floor streaming** to optimize transaction tips dynamically
6. **Submit transactions** using single or batched submission methods

## Key Features

### Async/Await Support

All examples use Python's asyncio for non-blocking operations and better performance.

### Type Hints

Complete type annotations for better code clarity and IDE support.

### Error Handling

Comprehensive error handling with retry logic and exponential backoff.

### Connection Management

Persistent connections with keepalive, health monitoring, and automatic recovery.

### Dynamic Tipping

Real-time tip optimization based on current network conditions from tip floor API.

## Best Practices

1. **Use async/await** - Leverage Python's asyncio for better performance
2. **Enable TLS** - Always use secure connections in production
3. **Implement keepalive** - Maintain persistent connections
4. **Monitor tip floors** - Use streaming API for dynamic tip adjustment
5. **Handle errors gracefully** - Implement proper retry logic
6. **Use type hints** - Improve code clarity and maintainability
7. **Choose appropriate endpoints** - Use the closest endpoint for better latency


# Connection

Establish a gRPC connection to NextBlock's API using Python and grpcio.

This page shows the connection pattern and authentication flow. Replace the placeholder client creation with the client generated from [`nextblock-proto`](https://github.com/nextblock-ag/nextblock-proto).

## Prerequisites

Install the required dependencies:

```bash
pip install grpcio grpcio-tools
pip install solders solana
pip install asyncio
```

Generate the Python gRPC client from the proto specs:

```bash
# Clone the proto repository
git clone https://github.com/nextblock-ag/nextblock-proto
cd nextblock-proto

# Generate Python gRPC client
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. *.proto
```

## Connection Setup

<pre class="language-python"><code class="lang-python"><strong>import asyncio
</strong>import grpc
from typing import Optional, Dict, Any
from dataclasses import dataclass
import ssl

<strong># Authentication metadata interceptor
</strong>class AuthInterceptor(grpc.aio.ClientInterceptor):
    def __init__(self, api_key: str):
        self.api_key = api_key
    
    async def intercept_unary_unary(self, continuation, client_call_details, request):
<strong>        # Add authorization header to all requests
</strong>        metadata = list(client_call_details.metadata or [])
        metadata.append(('authorization', self.api_key))
        
        new_details = client_call_details._replace(metadata=metadata)
        return await continuation(new_details, request)
    
    async def intercept_unary_stream(self, continuation, client_call_details, request):
<strong>        # Add authorization header to streaming requests
</strong>        metadata = list(client_call_details.metadata or [])
        metadata.append(('authorization', self.api_key))
        
        new_details = client_call_details._replace(metadata=metadata)
        return await continuation(new_details, request)

<strong># Connection configuration
</strong>@dataclass
class NextBlockConfig:
    endpoint: str = "frankfurt.nextblock.io:443"
    api_key: str = ""
    use_tls: bool = True
    timeout: float = 30.0
    keepalive_time_ms: int = 60000  # 60 seconds
    keepalive_timeout_ms: int = 15000  # 15 seconds
    
    @classmethod
    def from_env(cls) -> 'NextBlockConfig':
        import os
        return cls(
            endpoint=os.getenv('NEXTBLOCK_ENDPOINT', 'frankfurt.nextblock.io:443'),
            api_key=os.getenv('NEXTBLOCK_API_KEY', ''),
            use_tls=os.getenv('NEXTBLOCK_USE_TLS', 'true').lower() == 'true',
            timeout=float(os.getenv('NEXTBLOCK_TIMEOUT', '30')),
        )

<strong># Create secure channel with authentication
</strong>async def create_nextblock_channel(config: NextBlockConfig) -> grpc.aio.Channel:
<strong>    # Configure channel options
</strong>    options = [
        ('grpc.keepalive_time_ms', config.keepalive_time_ms),
        ('grpc.keepalive_timeout_ms', config.keepalive_timeout_ms),
        ('grpc.keepalive_permit_without_stream', True),
        ('grpc.http2.max_pings_without_data', 0),
        ('grpc.http2.min_ping_interval_without_data_ms', 300000),  # 5 minutes
    ]
    
    if config.use_tls:
<strong>        # Create secure channel with TLS
</strong>        credentials = grpc.ssl_channel_credentials()
        channel = grpc.aio.secure_channel(
            config.endpoint,
            credentials,
            options=options
        )
    else:
<strong>        # Create insecure channel
</strong>        channel = grpc.aio.insecure_channel(
            config.endpoint,
            options=options
        )
    
    return channel

<strong># Create authenticated client
</strong>async def create_nextblock_client(config: NextBlockConfig):
<strong>    # Create the channel
</strong>    channel = await create_nextblock_channel(config)
    
<strong>    # Add authentication interceptor
</strong>    auth_interceptor = AuthInterceptor(config.api_key)
    intercept_channel = grpc.aio.intercept_channel(channel, auth_interceptor)
    
<strong>    # Create the client stub
</strong>    # Import your generated gRPC client here
    # from your_generated_proto import api_pb2_grpc
    # client = api_pb2_grpc.ApiStub(intercept_channel)
    
    return intercept_channel, None  # Replace None with your generated client stub

<strong># Connection manager with health checking
</strong>class NextBlockConnectionManager:
    def __init__(self, config: NextBlockConfig):
        self.config = config
        self.channel: Optional[grpc.aio.Channel] = None
        self.client = None
        self.is_connected = False
        
    async def connect(self) -> bool:
<strong>        """Establish connection to NextBlock"""
</strong>        try:
            self.channel, self.client = await create_nextblock_client(self.config)
            
<strong>            # Test the connection
</strong>            await self.health_check()
            self.is_connected = True
            print(f"Successfully connected to NextBlock at {self.config.endpoint}")
            return True
            
        except Exception as e:
            print(f"Failed to connect to NextBlock: {e}")
            self.is_connected = False
            return False
    
    async def health_check(self) -> bool:
<strong>        """Check if the connection is healthy"""
</strong>        if not self.channel:
            return False
            
        try:
<strong>            # Test connection with a simple call
</strong>            # await self.client.ping(Empty())  # Uncomment with real client
            print("Connection health check passed")
            return True
        except Exception as e:
            print(f"Health check failed: {e}")
            return False
    
    async def disconnect(self):
<strong>        """Close the connection"""
</strong>        if self.channel:
            await self.channel.close()
            self.is_connected = False
            print("Disconnected from NextBlock")
    
    async def __aenter__(self):
        await self.connect()
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.disconnect()
</code></pre>

## Usage Example

<pre class="language-python"><code class="lang-python"><strong>async def main():
</strong><strong>    # Configure connection
</strong>    config = NextBlockConfig(
        endpoint="frankfurt.nextblock.io:443",
        api_key="&#x3C;your-api-key-here>",
        use_tls=True,
        timeout=30.0
    )
    
<strong>    # Method 1: Direct connection
</strong>    channel, client = await create_nextblock_client(config)
    
    try:
<strong>        # Use the client for API calls
</strong>        print("Connected to NextBlock!")
        
<strong>        # Your API calls would go here
</strong>        # response = await client.ping(Empty())
        
    finally:
        await channel.close()
    
<strong>    # Method 2: Using connection manager (recommended)
</strong>    async with NextBlockConnectionManager(config) as manager:
        if manager.is_connected:
            print("Connection manager established connection")
            
<strong>            # Use manager.client for API calls
</strong>            # response = await manager.client.ping(Empty())
            
        else:
            print("Failed to establish connection")

<strong># Run the example
</strong>if __name__ == "__main__":
    asyncio.run(main())
</code></pre>

## Advanced Connection Features

<pre class="language-python"><code class="lang-python"><strong>import logging
</strong>from typing import Callable, Any
import backoff

<strong># Enhanced connection manager with retry logic
</strong>class EnhancedConnectionManager(NextBlockConnectionManager):
    def __init__(self, config: NextBlockConfig, max_retries: int = 3):
        super().__init__(config)
        self.max_retries = max_retries
        self.retry_count = 0
        
    @backoff.on_exception(
        backoff.expo,
        (grpc.RpcError, ConnectionError),
        max_tries=3,
        base=2,
        max_value=60
    )
    async def connect_with_retry(self) -> bool:
<strong>        """Connect with exponential backoff retry"""
</strong>        self.retry_count += 1
        logging.info(f"Connection attempt {self.retry_count}")
        
        success = await self.connect()
        if not success:
            raise ConnectionError(f"Failed to connect on attempt {self.retry_count}")
        
        self.retry_count = 0  # Reset on success
        return True
    
    async def call_with_retry(self, func: Callable, *args, **kwargs) -> Any:
<strong>        """Execute gRPC call with automatic retry"""
</strong>        @backoff.on_exception(
            backoff.expo,
            grpc.RpcError,
            max_tries=3,
            giveup=lambda e: e.code() == grpc.StatusCode.UNAUTHENTICATED
        )
        async def _call():
            return await func(*args, **kwargs)
        
        return await _call()

<strong># Connection pool for high-throughput applications
</strong>class ConnectionPool:
    def __init__(self, config: NextBlockConfig, pool_size: int = 5):
        self.config = config
        self.pool_size = pool_size
        self.connections: List[NextBlockConnectionManager] = []
        self.current_index = 0
        
    async def initialize(self):
<strong>        """Initialize connection pool"""
</strong>        for i in range(self.pool_size):
            manager = NextBlockConnectionManager(self.config)
            if await manager.connect():
                self.connections.append(manager)
            else:
                logging.warning(f"Failed to create connection {i+1}/{self.pool_size}")
        
        logging.info(f"Connection pool initialized with {len(self.connections)} connections")
    
    def get_connection(self) -> NextBlockConnectionManager:
<strong>        """Get next available connection (round-robin)"""
</strong>        if not self.connections:
            raise RuntimeError("No available connections in pool")
        
        connection = self.connections[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.connections)
        return connection
    
    async def close_all(self):
<strong>        """Close all connections in the pool"""
</strong>        for connection in self.connections:
            await connection.disconnect()
        self.connections.clear()
</code></pre>

## Environment Configuration

<pre class="language-python"><code class="lang-python"><strong>import os
</strong>from typing import Dict, Any

<strong># Load configuration from environment variables
</strong>def load_config_from_env() -> NextBlockConfig:
    return NextBlockConfig(
        endpoint=os.getenv('NEXTBLOCK_ENDPOINT', 'frankfurt.nextblock.io:443'),
        api_key=os.getenv('NEXTBLOCK_API_KEY', ''),
        use_tls=os.getenv('NEXTBLOCK_USE_TLS', 'true').lower() == 'true',
        timeout=float(os.getenv('NEXTBLOCK_TIMEOUT', '30.0')),
        keepalive_time_ms=int(os.getenv('NEXTBLOCK_KEEPALIVE_TIME_MS', '60000')),
        keepalive_timeout_ms=int(os.getenv('NEXTBLOCK_KEEPALIVE_TIMEOUT_MS', '15000')),
    )

<strong># Configuration validation
</strong>def validate_config(config: NextBlockConfig) -> bool:
    if not config.api_key:
        raise ValueError("API key is required")
    
    if not config.endpoint:
        raise ValueError("Endpoint is required")
    
    if config.timeout &#x3C;= 0:
        raise ValueError("Timeout must be positive")
    
    return True

<strong># Example with configuration validation
</strong>async def main_with_validation():
    try:
        config = load_config_from_env()
        validate_config(config)
        
        async with EnhancedConnectionManager(config) as manager:
            await manager.connect_with_retry()
            print("Successfully connected with retry logic!")
            
    except ValueError as e:
        print(f"Configuration error: {e}")
    except Exception as e:
        print(f"Connection error: {e}")

if __name__ == "__main__":
    asyncio.run(main_with_validation())
</code></pre>

## Available Endpoints

* **Frankfurt**: `frankfurt.nextblock.io` (Europe)
* **Amsterdam**: `amsterdam.nextblock.io` (Europe)
* **London**: `london.nextblock.io` (Europe)
* **Singapore**: `singapore.nextblock.io` (Asia)
* **Tokyo**: `tokyo.nextblock.io` (Asia)
* **New York**: `ny.nextblock.io` (US East)
* **Salt Lake City**: `slc.nextblock.io` (US West)
* **Dublin**: `dublin.nextblock.io` (Europe)
* **Vilnius**: `vilnius.nextblock.io` (Europe)

## Best Practices

1. **Use async/await**: Leverage Python's asyncio for non-blocking operations
2. **Use TLS by default**: Prefer secure connections unless you explicitly operate in a trusted internal environment
3. **Implement keepalive**: Configure appropriate keepalive settings
4. **Handle errors gracefully**: Use retry logic with exponential backoff
5. **Validate configuration**: Check all required settings before connecting
6. **Use connection pooling**: For high-throughput applications
7. **Monitor connection health**: Implement regular health checks
8. **Close connections properly**: Always close connections when done


# QUIC Transaction Submission

Use QUIC when you already have signed Solana transaction bytes and want a low-overhead submission path from Python.

The client below:

1. Connects to a regional QUIC endpoint
2. Authenticates with your API key on a bidirectional stream
3. Reuses the connection to send raw transaction bytes on unidirectional streams

## Example

```python
import asyncio
import os
import ssl

import certifi
from aioquic.asyncio.client import connect
from aioquic.quic.configuration import QuicConfiguration

AUTH_OK = b"\x00"
MAX_TX_SIZE = 1232


class NextblockQuicClient:
    def __init__(self, client_cm, protocol):
        self._client_cm = client_cm
        self._protocol = protocol

    @classmethod
    async def connect(cls, host: str, port: int, api_key: str) -> "NextblockQuicClient":
        configuration = QuicConfiguration(
            is_client=True,
            alpn_protocols=["nb-tx/1"],
        )
        configuration.verify_mode = ssl.CERT_REQUIRED
        configuration.load_verify_locations(cafile=certifi.where())

        client_cm = connect(
            host,
            port,
            configuration=configuration,
            server_name=host,
        )
        protocol = await client_cm.__aenter__()

        reader, writer = await protocol.create_stream()
        writer.write(api_key.encode("utf-8"))
        await writer.drain()
        writer.write_eof()

        response = await reader.readexactly(1)
        if response != AUTH_OK:
            await client_cm.__aexit__(None, None, None)
            raise RuntimeError("authentication rejected")

        return cls(client_cm, protocol)

    async def send_transaction(self, raw_tx: bytes) -> None:
        if len(raw_tx) > MAX_TX_SIZE:
            raise ValueError(f"transaction too large: {len(raw_tx)}")

        _, writer = await self._protocol.create_stream(is_unidirectional=True)
        writer.write(raw_tx)
        await writer.drain()
        writer.write_eof()

    async def close(self) -> None:
        await self._client_cm.__aexit__(None, None, None)


async def main() -> None:
    api_key = os.environ.get("NEXTBLOCK_API_KEY")
    if not api_key:
        raise RuntimeError("Set NEXTBLOCK_API_KEY before running")

    client = await NextblockQuicClient.connect(
        "london.nextblock.io",
        11100,
        api_key,
    )

    try:
        # Replace this with the serialized bytes of your signed transaction.
        # Example with solders: raw_tx = bytes(signed_transaction)
        raw_tx = b"\x00\x01\x02\x03"
        await client.send_transaction(raw_tx)
        print("transaction queued")
    finally:
        await client.close()


asyncio.run(main())
```

## What To Replace

* Read the API key from `NEXTBLOCK_API_KEY` or your own config source.
* Pick the regional host closest to your deployment.
* Replace `raw_tx` with the serialized bytes of your signed transaction.

## Notes

* Send raw transaction bytes, not base64.
* Keep the QUIC connection open and reuse it for multiple transactions. This example closes after one send only to keep the sample short.
* QUIC does not support the extra gRPC submission flags or atomic bundle submission.
* If you already build transactions with `solders` or `solana-py`, serialize the signed transaction first and pass the bytes to `send_transaction()`.

See [QUIC Transaction Submission](/api/quic-transaction-submission) for the full endpoint list and protocol summary.


# Submit Single Transaction

Submit individual transactions to NextBlock using Python with proper tipping and error handling.

If you want to send raw signed transaction bytes over QUIC instead of gRPC, see [QUIC Transaction Submission](/api/examples/python/quic).

This example shows transaction construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

## Example

<pre class="language-python"><code class="lang-python"><strong>import asyncio
</strong>import base64
import random
from typing import List, Optional
from dataclasses import dataclass

from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
from solders.message import MessageV0
from solders.hash import Hash
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Finalized

<strong># NextBlock tip wallets for load balancing
</strong>NEXTBLOCK_TIP_WALLETS = [
    "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
    "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc",
    "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE",
    "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
    "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
    "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
    "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
    "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
]

<strong># Get random tip wallet for load balancing
</strong>def get_random_nextblock_tip_wallet() -> Pubkey:
    wallet_str = random.choice(NEXTBLOCK_TIP_WALLETS)
    return Pubkey.from_string(wallet_str)

<strong># Transaction submission parameters
</strong>@dataclass
class SubmissionOptions:
    skip_preflight: bool = True
    front_running_protection: bool = False
    revert_on_fail: bool = False
    disable_retries: bool = False
    snipe_transaction: bool = False

<strong># Build transaction with tip
</strong>async def build_transaction_with_tip(
    payer: Keypair,
    recent_blockhash: Hash,
    tip_amount: int,
    instructions: List,
) -> Transaction:
<strong>    # Create tip instruction (should be first)
</strong>    tip_wallet = get_random_nextblock_tip_wallet()
    tip_instruction = transfer(
        TransferParams(
            from_pubkey=payer.pubkey(),
            to_pubkey=tip_wallet,
            lamports=tip_amount
        )
    )
    
<strong>    # Combine all instructions
</strong>    all_instructions = [tip_instruction] + instructions
    
<strong>    # Create and sign transaction
</strong>    message = MessageV0.try_compile(
        payer=payer.pubkey(),
        instructions=all_instructions,
        address_lookup_table_accounts=[],
        recent_blockhash=recent_blockhash,
    )
    
    transaction = Transaction.new_unsigned(message)
    transaction.sign([payer], recent_blockhash)
    
    return transaction

<strong># Submit single transaction to NextBlock
</strong>async def submit_single_transaction(
    # nextblock_client,  # Your generated gRPC client
    rpc_client: AsyncClient,
    signer: Keypair,
    recipient: Pubkey,
    transfer_amount: int,
    tip_amount: int,
    options: SubmissionOptions = None,
) -> str:
    if options is None:
        options = SubmissionOptions()
    
<strong>    # Get recent blockhash
</strong>    response = await rpc_client.get_latest_blockhash(commitment=Finalized)
    recent_blockhash = response.value.blockhash
    
<strong>    # Create transfer instruction
</strong>    transfer_instruction = transfer(
        TransferParams(
            from_pubkey=signer.pubkey(),
            to_pubkey=recipient,
            lamports=transfer_amount
        )
    )
    
<strong>    # Build transaction with tip
</strong>    transaction = await build_transaction_with_tip(
        signer,
        recent_blockhash,
        tip_amount,
        [transfer_instruction]
    )
    
<strong>    # Convert to base64 for submission
</strong>    serialized_tx = bytes(transaction)
    base64_tx = base64.b64encode(serialized_tx).decode('utf-8')
    
<strong>    # Submit to NextBlock
</strong>    """ Uncomment when you have the generated gRPC client
    request = PostSubmitRequest(
        transaction=TransactionMessage(content=base64_tx),
        skip_pre_flight=options.skip_preflight,
        snipe_transaction=options.snipe_transaction,
        front_running_protection=options.front_running_protection,
        disable_retries=options.disable_retries,
        revert_on_fail=options.revert_on_fail,
    )
    
    response = await nextblock_client.post_submit_v2(request)
    
    print(f"Transaction submitted successfully!")
    print(f"Signature: {response.signature}")
    print(f"UUID: {response.uuid}")
    
    return response.signature
    """
    
<strong>    # Until your generated client is wired in, return the local signature
</strong>    signature = str(transaction.signatures[0])
    print(f"Local transaction built successfully: {signature}")
    return signature

<strong># Submit with optimal tip calculation
</strong>async def submit_with_optimal_tip(
    # nextblock_client,
    rpc_client: AsyncClient,
    signer: Keypair,
    recipient: Pubkey,
    transfer_amount: int,
    priority_level: str = "normal",
) -> str:
<strong>    # Calculate optimal tip based on priority level
</strong>    tip_amounts = {
        "conservative": 500_000,    # 0.0005 SOL
        "normal": 1_000_000,       # 0.001 SOL
        "aggressive": 2_000_000,   # 0.002 SOL
        "priority": 5_000_000,     # 0.005 SOL
    }
    
    tip_amount = tip_amounts.get(priority_level, tip_amounts["normal"])
    
    return await submit_single_transaction(
        # nextblock_client,
        rpc_client,
        signer,
        recipient,
        transfer_amount,
        tip_amount
    )

<strong># Batch multiple single transactions
</strong>async def submit_multiple_single_transactions(
    # nextblock_client,
    rpc_client: AsyncClient,
    signer: Keypair,
    transactions_data: List[tuple],  # [(recipient, amount, tip), ...]
) -> List[str]:
<strong>    """Submit multiple single transactions concurrently"""
</strong>    tasks = []
    
    for recipient, transfer_amount, tip_amount in transactions_data:
        task = submit_single_transaction(
            # nextblock_client,
            rpc_client,
            signer,
            recipient,
            transfer_amount,
            tip_amount
        )
        tasks.append(task)
    
<strong>    # Execute all transactions concurrently
</strong>    signatures = await asyncio.gather(*tasks, return_exceptions=True)
    
<strong>    # Handle results
</strong>    successful_signatures = []
    for i, result in enumerate(signatures):
        if isinstance(result, Exception):
            print(f"Transaction {i+1} failed: {result}")
        else:
            successful_signatures.append(result)
            print(f"Transaction {i+1} successful: {result}")
    
    return successful_signatures
</code></pre>

## Advanced Features

<pre class="language-python"><code class="lang-python"><strong>import time
</strong>from typing import Dict, Any
import backoff

<strong># Transaction with retry logic
</strong>@backoff.on_exception(
    backoff.expo,
    Exception,
    max_tries=3,
    base=2,
    max_value=30
)
async def submit_transaction_with_retry(
    # nextblock_client,
    rpc_client: AsyncClient,
    signer: Keypair,
    recipient: Pubkey,
    transfer_amount: int,
    tip_amount: int,
) -> str:
    return await submit_single_transaction(
        # nextblock_client,
        rpc_client,
        signer,
        recipient,
        transfer_amount,
        tip_amount
    )

<strong># Transaction builder with custom instructions
</strong>class TransactionBuilder:
    def __init__(self, payer: Keypair):
        self.payer = payer
        self.instructions = []
        self.tip_amount = 1_000_000  # Default tip
        
    def add_instruction(self, instruction):
<strong>        """Add custom instruction to transaction"""
</strong>        self.instructions.append(instruction)
        return self
    
    def set_tip_amount(self, amount: int):
<strong>        """Set tip amount in lamports"""
</strong>        self.tip_amount = amount
        return self
    
    async def build_and_submit(
        self,
        # nextblock_client,
        rpc_client: AsyncClient,
        options: SubmissionOptions = None
    ) -> str:
<strong>        """Build and submit the transaction"""
</strong>        if not self.instructions:
            raise ValueError("No instructions added to transaction")
        
<strong>        # Get recent blockhash
</strong>        response = await rpc_client.get_latest_blockhash(commitment=Finalized)
        recent_blockhash = response.value.blockhash
        
<strong>        # Build transaction with tip
</strong>        transaction = await build_transaction_with_tip(
            self.payer,
            recent_blockhash,
            self.tip_amount,
            self.instructions
        )
        
<strong>        # Convert and submit
</strong>        serialized_tx = bytes(transaction)
        base64_tx = base64.b64encode(serialized_tx).decode('utf-8')
        
<strong>        # Submit logic here (similar to above)
</strong>        signature = str(transaction.signatures[0])
        print(f"Custom transaction submitted: {signature}")
        return signature

<strong># Performance monitoring
</strong>class TransactionMetrics:
    def __init__(self):
        self.submissions = 0
        self.successes = 0
        self.failures = 0
        self.total_time = 0.0
        self.start_time = None
    
    def start_submission(self):
        self.submissions += 1
        self.start_time = time.time()
    
    def record_success(self):
        if self.start_time:
            self.total_time += time.time() - self.start_time
            self.successes += 1
            self.start_time = None
    
    def record_failure(self):
        if self.start_time:
            self.total_time += time.time() - self.start_time
            self.failures += 1
            self.start_time = None
    
    def get_stats(self) -> Dict[str, Any]:
        return {
            "total_submissions": self.submissions,
            "successes": self.successes,
            "failures": self.failures,
            "success_rate": self.successes / max(self.submissions, 1) * 100,
            "average_time": self.total_time / max(self.successes, 1),
        }

<strong># Monitored transaction submission
</strong>async def submit_with_monitoring(
    # nextblock_client,
    rpc_client: AsyncClient,
    signer: Keypair,
    recipient: Pubkey,
    transfer_amount: int,
    tip_amount: int,
    metrics: TransactionMetrics,
) -> Optional[str]:
    metrics.start_submission()
    
    try:
        signature = await submit_single_transaction(
            # nextblock_client,
            rpc_client,
            signer,
            recipient,
            transfer_amount,
            tip_amount
        )
        metrics.record_success()
        return signature
        
    except Exception as e:
        metrics.record_failure()
        print(f"Transaction failed: {e}")
        return None
</code></pre>

## Usage Examples

<pre class="language-python"><code class="lang-python"><strong>async def main():
</strong><strong>    # Initialize clients
</strong>    rpc_client = AsyncClient("https://api.mainnet-beta.solana.com")
    signer = Keypair()  # Use your actual keypair
    recipient = Pubkey.from_string("&#x3C;recipient-public-key>")
    
<strong>    # Connect to NextBlock (see connection.md)
</strong>    # config = NextBlockConfig.from_env()
    # async with NextBlockConnectionManager(config) as manager:
    #     nextblock_client = manager.client
    
    try:
<strong>        # Example 1: Basic transaction submission
</strong>        signature1 = await submit_single_transaction(
            # nextblock_client,
            rpc_client,
            signer,
            recipient,
            10_000,     # Transfer 10,000 lamports
            1_000_000,  # Tip 1,000,000 lamports (0.001 SOL)
        )
        print(f"Basic transaction: {signature1}")
        
<strong>        # Example 2: Transaction with optimal tip
</strong>        signature2 = await submit_with_optimal_tip(
            # nextblock_client,
            rpc_client,
            signer,
            recipient,
            20_000,      # Transfer 20,000 lamports
            "aggressive" # Use aggressive tip strategy
        )
        print(f"Optimally tipped transaction: {signature2}")
        
<strong>        # Example 3: Custom transaction builder
</strong>        builder = TransactionBuilder(signer)
        signature3 = await (builder
            .add_instruction(transfer(TransferParams(
                from_pubkey=signer.pubkey(),
                to_pubkey=recipient,
                lamports=30_000
            )))
            .set_tip_amount(1_500_000)
            .build_and_submit(
                # nextblock_client,
                rpc_client
            ))
        print(f"Custom built transaction: {signature3}")
        
<strong>        # Example 4: Multiple transactions
</strong>        transactions_data = [
            (recipient, 5_000, 500_000),
            (recipient, 7_500, 750_000),
            (recipient, 12_500, 1_250_000),
        ]
        
        signatures = await submit_multiple_single_transactions(
            # nextblock_client,
            rpc_client,
            signer,
            transactions_data
        )
        print(f"Multiple transactions: {signatures}")
        
<strong>        # Example 5: Transaction with monitoring
</strong>        metrics = TransactionMetrics()
        
        for i in range(5):
            await submit_with_monitoring(
                # nextblock_client,
                rpc_client,
                signer,
                recipient,
                1_000 * (i + 1),  # Varying amounts
                500_000,          # Fixed tip
                metrics
            )
        
        print(f"Performance metrics: {metrics.get_stats()}")
        
    finally:
        await rpc_client.close()

<strong># Run the examples
</strong>if __name__ == "__main__":
    asyncio.run(main())
</code></pre>

## Best Practices

1. **Always include tips**: NextBlock prioritizes transactions with appropriate tips
2. **Use random tip wallets**: Distribute load across multiple tip addresses
3. **Monitor tip floors**: Adjust tips based on current network conditions
4. **Handle errors gracefully**: Implement retry logic with exponential backoff
5. **Validate inputs**: Always validate public keys and amounts before submission
6. **Use async/await**: Leverage Python's asyncio for better performance
7. **Monitor performance**: Track success rates and response times
8. **Choose appropriate RPC endpoints**: Use reliable RPC providers for blockhash retrieval


# Submit Batched Transactions

Submit 2-4 transactions as an atomic bundle to NextBlock using Python. Batched transactions are processed as Jito bundles - either all succeed or none do.

This example shows bundle construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

## Example

<pre class="language-python"><code class="lang-python"><strong>import asyncio
</strong>import base64
import random
from typing import List, Optional, Tuple
from dataclasses import dataclass

from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
from solders.message import MessageV0
from solders.hash import Hash
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Finalized

<strong># NextBlock tip wallets
</strong>NEXTBLOCK_TIP_WALLETS = [
    "NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid",
    "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc",
    "NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE",
    "NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2",
    "NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X",
    "NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb",
    "neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At",
    "nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG",
]

<strong># Transaction bundle builder
</strong>class TransactionBundle:
    def __init__(self):
        self.transactions: List[Transaction] = []
        self.max_size = 4
        self.min_size = 2
    
    def add_transaction(self, transaction: Transaction) -> 'TransactionBundle':
<strong>        """Add transaction to bundle"""
</strong>        if len(self.transactions) >= self.max_size:
            raise ValueError(f"Bundle cannot contain more than {self.max_size} transactions")
        
        self.transactions.append(transaction)
        return self
    
    def validate(self) -> bool:
<strong>        """Validate bundle before submission"""
</strong>        if len(self.transactions) &#x3C; self.min_size:
            raise ValueError(f"Bundle must contain at least {self.min_size} transactions")
        
        if len(self.transactions) > self.max_size:
            raise ValueError(f"Bundle cannot contain more than {self.max_size} transactions")
        
<strong>        # Check for duplicate signatures
</strong>        signatures = set()
        for tx in self.transactions:
            sig = str(tx.signatures[0])
            if sig in signatures:
                raise ValueError("Duplicate transaction signatures in bundle")
            signatures.add(sig)
        
        return True
    
    def to_base64_transactions(self) -> List[str]:
<strong>        """Convert all transactions to base64 strings"""
</strong>        self.validate()
        
        base64_transactions = []
        for tx in self.transactions:
            serialized_tx = bytes(tx)
            base64_tx = base64.b64encode(serialized_tx).decode('utf-8')
            base64_transactions.append(base64_tx)
        
        return base64_transactions
    
    def get_signatures(self) -> List[str]:
<strong>        """Get all transaction signatures"""
</strong>        return [str(tx.signatures[0]) for tx in self.transactions]

<strong># Build single transaction with tip
</strong>async def build_transaction_with_tip(
    payer: Keypair,
    recent_blockhash: Hash,
    tip_amount: int,
    instructions: List,
) -> Transaction:
<strong>    # Random tip wallet for load balancing
</strong>    tip_wallet = Pubkey.from_string(random.choice(NEXTBLOCK_TIP_WALLETS))
    
<strong>    # Create tip instruction (should be first)
</strong>    tip_instruction = transfer(
        TransferParams(
            from_pubkey=payer.pubkey(),
            to_pubkey=tip_wallet,
            lamports=tip_amount
        )
    )
    
<strong>    # Combine all instructions
</strong>    all_instructions = [tip_instruction] + instructions
    
<strong>    # Create and sign transaction
</strong>    message = MessageV0.try_compile(
        payer=payer.pubkey(),
        instructions=all_instructions,
        address_lookup_table_accounts=[],
        recent_blockhash=recent_blockhash,
    )
    
    transaction = Transaction.new_unsigned(message)
    transaction.sign([payer], recent_blockhash)
    
    return transaction

<strong># Submit batched transactions
</strong>async def submit_batched_transactions(
    # nextblock_client,  # Your generated gRPC client
    rpc_client: AsyncClient,
    signer: Keypair,
    transaction_specs: List[Tuple[List, int]],  # [(instructions, tip_amount), ...]
) -> str:
<strong>    """Submit multiple transactions as an atomic bundle"""
</strong>    
<strong>    # Get recent blockhash (same for all transactions in bundle)
</strong>    response = await rpc_client.get_latest_blockhash(commitment=Finalized)
    recent_blockhash = response.value.blockhash
    
<strong>    # Build transaction bundle
</strong>    bundle = TransactionBundle()
    
    for instructions, tip_amount in transaction_specs:
        transaction = await build_transaction_with_tip(
            signer,
            recent_blockhash,
            tip_amount,
            instructions
        )
        bundle.add_transaction(transaction)
    
<strong>    # Get base64 transactions for submission
</strong>    base64_transactions = bundle.to_base64_transactions()
    signatures = bundle.get_signatures()
    
<strong>    # Log transaction signatures
</strong>    for i, sig in enumerate(signatures):
        print(f"Transaction {i+1} signature: {sig}")
    
<strong>    # Submit bundle to NextBlock
</strong>    """ Uncomment when you have the generated gRPC client
    entries = [
        PostSubmitRequestEntry(
            transaction=TransactionMessage(content=base64_tx)
        )
        for base64_tx in base64_transactions
    ]
    
    request = PostSubmitBatchRequest(entries=entries)
    response = await nextblock_client.post_submit_batch_v2(request)
    
    print(f"Batch submitted successfully!")
    print(f"Bundle signature: {response.signature}")
    
    return response.signature
    """
    
<strong>    # Until your generated client is wired in, return a placeholder bundle signature
</strong>    bundle_signature = ""
    print(f"Local bundle prepared with {len(bundle.transactions)} transactions.")
    return bundle_signature

<strong># Build common transaction patterns
</strong>async def build_arbitrage_bundle(
    # nextblock_client,
    rpc_client: AsyncClient,
    signer: Keypair,
    dex_a_address: Pubkey,
    dex_b_address: Pubkey,
    trade_amount: int,
) -> str:
<strong>    """Build arbitrage bundle for cross-DEX trading"""
</strong>    
<strong>    # Transaction 1: Buy on DEX A
</strong>    buy_instructions = [
<strong>        # Add your DEX-specific buy instructions here
</strong>        transfer(TransferParams(
            from_pubkey=signer.pubkey(),
            to_pubkey=dex_a_address,
            lamports=trade_amount
        ))
    ]
    
<strong>    # Transaction 2: Sell on DEX B
</strong>    sell_instructions = [
<strong>        # Add your DEX-specific sell instructions here
</strong>        transfer(TransferParams(
            from_pubkey=signer.pubkey(),
            to_pubkey=dex_b_address,
            lamports=trade_amount
        ))
    ]
    
    transaction_specs = [
        (buy_instructions, 2_000_000),   # Higher tip for arbitrage
        (sell_instructions, 2_000_000),  # Higher tip for arbitrage
    ]
    
    return await submit_batched_transactions(
        # nextblock_client,
        rpc_client,
        signer,
        transaction_specs
    )

<strong># Build complex DeFi operation bundle
</strong>async def build_defi_operation_bundle(
    # nextblock_client,
    rpc_client: AsyncClient,
    signer: Keypair,
) -> str:
<strong>    """Build complex DeFi operation bundle"""
</strong>    
<strong>    # Transaction 1: Setup - Create token accounts
</strong>    setup_instructions = [
<strong>        # Add token account creation instructions
</strong>        transfer(TransferParams(
            from_pubkey=signer.pubkey(),
            to_pubkey=Pubkey.from_string("&#x3C;token-program-address>"),
            lamports=1_000_000  # Rent exemption
        ))
    ]
    
<strong>    # Transaction 2: Main operation - Execute swap
</strong>    swap_instructions = [
<strong>        # Add swap instructions (Jupiter, Raydium, etc.)
</strong>        transfer(TransferParams(
            from_pubkey=signer.pubkey(),
            to_pubkey=Pubkey.from_string("&#x3C;swap-program-address>"),
            lamports=0  # No SOL transfer for swap
        ))
    ]
    
<strong>    # Transaction 3: Cleanup - Stake or provide liquidity
</strong>    stake_instructions = [
<strong>        # Add staking/liquidity instructions
</strong>        transfer(TransferParams(
            from_pubkey=signer.pubkey(),
            to_pubkey=Pubkey.from_string("&#x3C;stake-pool-address>"),
            lamports=0
        ))
    ]
    
    transaction_specs = [
        (setup_instructions, 500_000),    # Setup tip
        (swap_instructions, 1_500_000),   # Main operation tip
        (stake_instructions, 750_000),    # Cleanup tip
    ]
    
    return await submit_batched_transactions(
        # nextblock_client,
        rpc_client,
        signer,
        transaction_specs
    )
</code></pre>

## Advanced Bundle Management

<pre class="language-python"><code class="lang-python"><strong># Bundle with conditional execution
</strong>class ConditionalBundle(TransactionBundle):
    def __init__(self):
        super().__init__()
        self.conditions: List[callable] = []
    
    def add_conditional_transaction(
        self, 
        transaction: Transaction, 
        condition: callable
    ) -> 'ConditionalBundle':
<strong>        """Add transaction that only executes if condition is met"""
</strong>        self.add_transaction(transaction)
        self.conditions.append(condition)
        return self
    
    async def evaluate_conditions(self, rpc_client: AsyncClient) -> List[bool]:
<strong>        """Evaluate all conditions before submission"""
</strong>        results = []
        for condition in self.conditions:
            try:
                result = await condition(rpc_client) if asyncio.iscoroutinefunction(condition) else condition()
                results.append(bool(result))
            except Exception as e:
                print(f"Condition evaluation failed: {e}")
                results.append(False)
        return results
    
    def filter_by_conditions(self, condition_results: List[bool]) -> 'TransactionBundle':
<strong>        """Create new bundle with only transactions that meet conditions"""
</strong>        filtered_bundle = TransactionBundle()
        
        for i, (transaction, condition_met) in enumerate(zip(self.transactions, condition_results)):
            if condition_met:
                filtered_bundle.add_transaction(transaction)
            else:
                print(f"Transaction {i+1} filtered out due to condition")
        
        return filtered_bundle

<strong># Bundle performance optimizer
</strong>class BundleOptimizer:
    def __init__(self):
        self.tip_multipliers = {
            "setup": 0.5,      # Lower priority
            "main": 1.5,       # Higher priority
            "cleanup": 0.75,   # Medium priority
            "arbitrage": 2.0,  # Highest priority
        }
    
    def optimize_tips(
        self, 
        base_tip: int, 
        transaction_types: List[str]
    ) -> List[int]:
<strong>        """Optimize tip amounts based on transaction types"""
</strong>        optimized_tips = []
        
        for tx_type in transaction_types:
            multiplier = self.tip_multipliers.get(tx_type, 1.0)
            optimized_tip = int(base_tip * multiplier)
            optimized_tips.append(optimized_tip)
        
        return optimized_tips
    
    def reorder_transactions(
        self, 
        transactions: List[Transaction], 
        transaction_types: List[str]
    ) -> Tuple[List[Transaction], List[str]]:
<strong>        """Reorder transactions for optimal execution"""
</strong><strong>        # Priority order: setup -> main -> cleanup
</strong>        priority_order = {"setup": 1, "main": 2, "cleanup": 3, "arbitrage": 0}
        
<strong>        # Create pairs and sort by priority
</strong>        tx_pairs = list(zip(transactions, transaction_types))
        tx_pairs.sort(key=lambda x: priority_order.get(x[1], 2))
        
<strong>        # Unzip the sorted pairs
</strong>        sorted_transactions, sorted_types = zip(*tx_pairs)
        return list(sorted_transactions), list(sorted_types)

<strong># Bundle status monitoring
</strong>@dataclass
class BundleStatus:
    bundle_id: str
    transaction_count: int
    submitted_at: float
    signatures: List[str]
    status: str = "pending"  # pending, confirmed, failed
    
    def is_complete(self) -> bool:
        return self.status in ["confirmed", "failed"]

class BundleTracker:
    def __init__(self):
        self.bundles: Dict[str, BundleStatus] = {}
    
    def track_bundle(self, bundle_status: BundleStatus):
<strong>        """Start tracking a bundle"""
</strong>        self.bundles[bundle_status.bundle_id] = bundle_status
    
    async def check_bundle_status(
        self, 
        bundle_id: str, 
        rpc_client: AsyncClient
    ) -> Optional[BundleStatus]:
<strong>        """Check the status of a tracked bundle"""
</strong>        if bundle_id not in self.bundles:
            return None
        
        bundle_status = self.bundles[bundle_id]
        
<strong>        # Check if all transactions are confirmed
</strong>        confirmed_count = 0
        for signature in bundle_status.signatures:
            try:
<strong>                # Check transaction status
</strong>                # response = await rpc_client.get_signature_status(signature)
                # if response.value and response.value.confirmation_status:
                #     confirmed_count += 1
                confirmed_count += 1  # Mock confirmation
            except Exception as e:
                print(f"Failed to check signature {signature}: {e}")
        
<strong>        # Update bundle status
</strong>        if confirmed_count == bundle_status.transaction_count:
            bundle_status.status = "confirmed"
        elif time.time() - bundle_status.submitted_at > 60:  # Timeout after 60 seconds
            bundle_status.status = "failed"
        
        return bundle_status
</code></pre>

## Usage Examples

<pre class="language-python"><code class="lang-python"><strong>async def main():
</strong><strong>    # Initialize clients
</strong>    rpc_client = AsyncClient("https://api.mainnet-beta.solana.com")
    signer = Keypair()  # Use your actual keypair
    
<strong>    # Connect to NextBlock (see connection.md)
</strong>    # config = NextBlockConfig.from_env()
    # async with NextBlockConnectionManager(config) as manager:
    #     nextblock_client = manager.client
    
    try:
<strong>        # Example 1: Basic batch submission
</strong>        transaction_specs = [
<strong>            # Setup transaction
</strong>            ([transfer(TransferParams(
                from_pubkey=signer.pubkey(),
                to_pubkey=Pubkey.from_string("&#x3C;recipient1>"),
                lamports=100_000
            ))], 500_000),  # 0.0005 SOL tip
            
<strong>            # Main transaction
</strong>            ([transfer(TransferParams(
                from_pubkey=signer.pubkey(),
                to_pubkey=Pubkey.from_string("&#x3C;recipient2>"),
                lamports=200_000
            ))], 1_000_000),  # 0.001 SOL tip
            
<strong>            # Cleanup transaction
</strong>            ([transfer(TransferParams(
                from_pubkey=signer.pubkey(),
                to_pubkey=Pubkey.from_string("&#x3C;recipient3>"),
                lamports=50_000
            ))], 500_000),  # 0.0005 SOL tip
        ]
        
        bundle_signature = await submit_batched_transactions(
            # nextblock_client,
            rpc_client,
            signer,
            transaction_specs
        )
        print(f"Basic batch: {bundle_signature}")
        
<strong>        # Example 2: Arbitrage bundle
</strong>        arb_signature = await build_arbitrage_bundle(
            # nextblock_client,
            rpc_client,
            signer,
            Pubkey.from_string("&#x3C;dex-a-address>"),
            Pubkey.from_string("&#x3C;dex-b-address>"),
            1_000_000  # 0.001 SOL trade
        )
        print(f"Arbitrage bundle: {arb_signature}")
        
<strong>        # Example 3: Optimized DeFi bundle
</strong>        defi_signature = await build_defi_operation_bundle(
            # nextblock_client,
            rpc_client,
            signer
        )
        print(f"DeFi bundle: {defi_signature}")
        
<strong>        # Example 4: Bundle with optimization
</strong>        optimizer = BundleOptimizer()
        base_tip = 1_000_000
        transaction_types = ["setup", "main", "cleanup"]
        optimized_tips = optimizer.optimize_tips(base_tip, transaction_types)
        
        print(f"Optimized tips: {optimized_tips}")
        
    finally:
        await rpc_client.close()

if __name__ == "__main__":
    asyncio.run(main())
</code></pre>

## Best Practices

1. **Bundle size limits**: Keep bundles between 2-4 transactions for optimal success rates
2. **Transaction ordering**: Setup → Main operations → Cleanup
3. **Progressive tipping**: Use higher tips for more critical transactions
4. **Error handling**: Validate bundles before submission
5. **Performance monitoring**: Track bundle success rates and timing
6. **Conditional execution**: Filter transactions based on current conditions
7. **Tip optimization**: Adjust tips based on transaction importance and network conditions


# Tip Floor Stream

Stream real-time tip floor data from NextBlock to optimize transaction tips dynamically using Python.

<pre class="language-python"><code class="lang-python"><strong>import asyncio
</strong>import json
import time
from typing import Dict, Any, Optional, List
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
import logging

<strong># Tip floor data structure
</strong>@dataclass
class TipFloorData:
    time: str
    landed_tips_25th_percentile: float
    landed_tips_50th_percentile: float
    landed_tips_75th_percentile: float
    landed_tips_95th_percentile: float
    landed_tips_99th_percentile: float
    ema_landed_tips_50th_percentile: float
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'TipFloorData':
        return cls(**data)
    
    def to_lamports(self, percentile: str) -> int:
<strong>        """Convert SOL amounts to lamports"""
</strong>        sol_amount = getattr(self, f"landed_tips_{percentile}_percentile")
        return int(sol_amount * 1_000_000_000)

<strong># Tip strategy management
</strong>@dataclass
class TipStrategy:
    conservative_tip: int = 500_000     # 25th percentile
    normal_tip: int = 1_000_000        # 50th percentile
    aggressive_tip: int = 2_000_000    # 75th percentile
    priority_tip: int = 5_000_000      # 95th percentile
    last_updated: datetime = None
    
    def update_from_tip_floor(self, tip_floor: TipFloorData):
<strong>        """Update strategy based on tip floor data"""
</strong>        self.conservative_tip = tip_floor.to_lamports("25th")
        self.normal_tip = tip_floor.to_lamports("50th")
        self.aggressive_tip = tip_floor.to_lamports("75th")
        self.priority_tip = tip_floor.to_lamports("95th")
        self.last_updated = datetime.now(timezone.utc)
    
    def get_tip_for_priority(self, priority: str) -> int:
<strong>        """Get tip amount for given priority level"""
</strong>        return {
            "conservative": self.conservative_tip,
            "normal": self.normal_tip,
            "aggressive": self.aggressive_tip,
            "priority": self.priority_tip,
        }.get(priority, self.normal_tip)

<strong># Global tip strategy instance
</strong>global_tip_strategy = TipStrategy()

<strong># Stream tip floor data
</strong>async def stream_tip_floor(
    # nextblock_client,  # Your generated gRPC client
    update_frequency: str = "1m",
    callback: Optional[callable] = None,
) -> None:
<strong>    """Stream tip floor updates from NextBlock"""
</strong>    print(f"Starting tip floor stream with frequency: {update_frequency}")
    
    """ Uncomment when you have the generated gRPC client
    try:
        request = TipFloorStreamRequest(update_frequency=update_frequency)
        stream = nextblock_client.stream_tip_floor(request)
        
        print("Streaming tip floor data:")
        
        async for tip_floor_response in stream:
            try:
                # Convert protobuf response to TipFloorData
                tip_floor = TipFloorData(
                    time=tip_floor_response.time,
                    landed_tips_25th_percentile=tip_floor_response.landed_tips_25th_percentile,
                    landed_tips_50th_percentile=tip_floor_response.landed_tips_50th_percentile,
                    landed_tips_75th_percentile=tip_floor_response.landed_tips_75th_percentile,
                    landed_tips_95th_percentile=tip_floor_response.landed_tips_95th_percentile,
                    landed_tips_99th_percentile=tip_floor_response.landed_tips_99th_percentile,
                    ema_landed_tips_50th_percentile=tip_floor_response.ema_landed_tips_50th_percentile,
                )
                
                print(f"Received tip floor update:")
                print(f"  Time: {tip_floor.time}")
                print(f"  25th percentile: {tip_floor.landed_tips_25th_percentile:.6f} SOL")
                print(f"  50th percentile: {tip_floor.landed_tips_50th_percentile:.6f} SOL")
                print(f"  75th percentile: {tip_floor.landed_tips_75th_percentile:.6f} SOL")
                print(f"  95th percentile: {tip_floor.landed_tips_95th_percentile:.6f} SOL")
                print(f"  EMA 50th percentile: {tip_floor.ema_landed_tips_50th_percentile:.6f} SOL")
                print("  ---")
                
                # Update global tip strategy
                await process_tip_floor_update(tip_floor)
                
                # Call custom callback if provided
                if callback:
                    await callback(tip_floor)
                    
            except Exception as e:
                logging.error(f"Error processing tip floor update: {e}")
                
    except Exception as e:
        logging.error(f"Tip floor stream error: {e}")
        # Implement reconnection logic here
    """
    
<strong>    # Mock streaming for demonstration
</strong>    print("Mock tip floor streaming started...")
    
    while True:
        await asyncio.sleep(60)  # Update every minute
        
<strong>        # Generate mock tip floor data
</strong>        mock_tip_floor = TipFloorData(
            time=datetime.now(timezone.utc).isoformat(),
            landed_tips_25th_percentile=0.0011,
            landed_tips_50th_percentile=0.005000001,
            landed_tips_75th_percentile=0.01555,
            landed_tips_95th_percentile=0.09339195639999975,
            landed_tips_99th_percentile=0.4846427910400001,
            ema_landed_tips_50th_percentile=0.005989477267191758,
        )
        
        print(f"Mock tip floor update: {asdict(mock_tip_floor)}")
        await process_tip_floor_update(mock_tip_floor)
        
        if callback:
            await callback(mock_tip_floor)

<strong># Process tip floor updates
</strong>async def process_tip_floor_update(tip_floor: TipFloorData) -> None:
<strong>    """Process incoming tip floor data"""
</strong><strong>    # Update global strategy
</strong>    global_tip_strategy.update_from_tip_floor(tip_floor)
    
<strong>    # Log the update
</strong>    logging.info(f"Updated tip strategy at {tip_floor.time}")
    logging.info(f"  Conservative: {global_tip_strategy.conservative_tip} lamports")
    logging.info(f"  Normal: {global_tip_strategy.normal_tip} lamports")
    logging.info(f"  Aggressive: {global_tip_strategy.aggressive_tip} lamports")
    logging.info(f"  Priority: {global_tip_strategy.priority_tip} lamports")
    
<strong>    # Store historical data
</strong>    await store_tip_floor_data(tip_floor)
    
<strong>    # Trigger any pending transactions
</strong>    await trigger_pending_transactions()

<strong># Historical data management
</strong>class TipFloorHistory:
    def __init__(self, max_size: int = 1000):
        self.data: List[TipFloorData] = []
        self.max_size = max_size
    
    def add(self, tip_floor: TipFloorData):
<strong>        """Add tip floor data to history"""
</strong>        if len(self.data) >= self.max_size:
            self.data.pop(0)  # Remove oldest
        self.data.append(tip_floor)
    
    def get_trend(self, percentile: str = "50th", window: int = 10) -> float:
<strong>        """Calculate tip trend over time window"""
</strong>        if len(self.data) &#x3C; 2:
            return 0.0
        
        recent_data = self.data[-window:] if len(self.data) >= window else self.data
        
        if len(recent_data) &#x3C; 2:
            return 0.0
        
        start_value = getattr(recent_data[0], f"landed_tips_{percentile}_percentile")
        end_value = getattr(recent_data[-1], f"landed_tips_{percentile}_percentile")
        
        return end_value - start_value
    
    def get_average(self, percentile: str = "50th", window: int = 10) -> float:
<strong>        """Get average tip over time window"""
</strong>        if not self.data:
            return 0.0
        
        recent_data = self.data[-window:] if len(self.data) >= window else self.data
        values = [getattr(d, f"landed_tips_{percentile}_percentile") for d in recent_data]
        
        return sum(values) / len(values)

<strong># Global history tracker
</strong>tip_floor_history = TipFloorHistory()

<strong># Smart tip calculation with trend analysis
</strong>async def get_smart_tip(
    base_priority: str = "normal",
    consider_trend: bool = True,
) -> int:
<strong>    """Calculate smart tip amount based on current data and trends"""
</strong>    base_tip = global_tip_strategy.get_tip_for_priority(base_priority)
    
    if not consider_trend or len(tip_floor_history.data) &#x3C; 2:
        return base_tip
    
<strong>    # Analyze trend
</strong>    trend = tip_floor_history.get_trend("50th", window=5)
    
<strong>    # Adjust tip based on trend
</strong>    if trend > 0.001:  # Tips increasing
        adjustment_factor = 1.2
        print(f"Tips trending up (+{trend:.6f}), increasing tip by 20%")
    elif trend &#x3C; -0.001:  # Tips decreasing
        adjustment_factor = 0.9
        print(f"Tips trending down ({trend:.6f}), decreasing tip by 10%")
    else:
        adjustment_factor = 1.0
        print(f"Tips stable ({trend:.6f}), no adjustment")
    
    smart_tip = int(base_tip * adjustment_factor)
    return max(smart_tip, 100_000)  # Minimum tip of 0.0001 SOL

<strong># Store tip floor data
</strong>async def store_tip_floor_data(tip_floor: TipFloorData) -> None:
<strong>    """Store tip floor data for analysis"""
</strong><strong>    # Add to history
</strong>    tip_floor_history.add(tip_floor)
    
<strong>    # Optionally save to file
</strong>    filename = f"tip_data_{datetime.now().strftime('%Y%m%d')}.jsonl"
    
    with open(filename, "a") as f:
        json.dump(asdict(tip_floor), f)
        f.write("\n")

<strong># Trigger pending transactions
</strong>async def trigger_pending_transactions() -> None:
<strong>    """Check and trigger any pending transactions with updated tips"""
</strong>    print("Checking for pending transactions to trigger...")
<strong>    # Implementation would check your pending transaction queue
</strong>    # and submit them with updated tip amounts
</code></pre>

## Usage Example

<pre class="language-python"><code class="lang-python"><strong>async def tip_floor_example():
</strong><strong>    # Connect to NextBlock (see connection.md)
</strong>    # config = NextBlockConfig.from_env()
    # async with NextBlockConnectionManager(config) as manager:
    #     nextblock_client = manager.client
    
<strong>    # Custom callback for tip floor updates
</strong>    async def on_tip_floor_update(tip_floor: TipFloorData):
        print(f"Custom handler: Received update at {tip_floor.time}")
        
<strong>        # Example: Trigger high-priority transactions when tips are low
</strong>        if tip_floor.landed_tips_50th_percentile &#x3C; 0.002:  # Less than 0.002 SOL
            print("Tips are low - good time for high-priority transactions!")
            # await submit_priority_transactions()
    
<strong>    # Start streaming in background
</strong>    stream_task = asyncio.create_task(
        stream_tip_floor(
            # nextblock_client,
            update_frequency="1m",
            callback=on_tip_floor_update
        )
    )
    
<strong>    # Example usage of dynamic tips
</strong>    await asyncio.sleep(5)  # Wait for initial data
    
<strong>    # Get current optimal tips
</strong>    conservative_tip = global_tip_strategy.get_tip_for_priority("conservative")
    normal_tip = global_tip_strategy.get_tip_for_priority("normal")
    aggressive_tip = global_tip_strategy.get_tip_for_priority("aggressive")
    
    print(f"Current optimal tips:")
    print(f"  Conservative: {conservative_tip} lamports")
    print(f"  Normal: {normal_tip} lamports")
    print(f"  Aggressive: {aggressive_tip} lamports")
    
<strong>    # Get smart tip with trend analysis
</strong>    smart_tip = await get_smart_tip("normal", consider_trend=True)
    print(f"  Smart tip: {smart_tip} lamports")
    
<strong>    # Keep streaming
</strong>    try:
        await stream_task
    except KeyboardInterrupt:
        stream_task.cancel()
        print("Tip floor streaming stopped")

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    asyncio.run(tip_floor_example())
</code></pre>


# Keepalive

Maintain persistent gRPC connections to NextBlock for optimal performance using Python's asyncio.

<pre class="language-python"><code class="lang-python"><strong>import asyncio
</strong>import time
import logging
from typing import Optional
from dataclasses import dataclass, field
from datetime import datetime, timezone

<strong># Connection health tracking
</strong>@dataclass
class ConnectionHealth:
    is_healthy: bool = True
    last_successful_ping: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    consecutive_failures: int = 0
    total_pings_sent: int = 0
    total_pings_successful: int = 0
    average_ping_time: float = 0.0
    
    def success_rate(self) -> float:
<strong>        """Calculate ping success rate percentage"""
</strong>        if self.total_pings_sent == 0:
            return 100.0
        return (self.total_pings_successful / self.total_pings_sent) * 100.0
    
    def update_success(self, ping_time: float):
<strong>        """Update health after successful ping"""
</strong>        self.is_healthy = True
        self.last_successful_ping = datetime.now(timezone.utc)
        self.consecutive_failures = 0
        self.total_pings_sent += 1
        self.total_pings_successful += 1
        
<strong>        # Update average ping time
</strong>        if self.total_pings_successful == 1:
            self.average_ping_time = ping_time
        else:
            self.average_ping_time = (
                (self.average_ping_time * (self.total_pings_successful - 1) + ping_time) 
                / self.total_pings_successful
            )
    
    def update_failure(self, max_failures: int = 3):
<strong>        """Update health after failed ping"""
</strong>        self.consecutive_failures += 1
        self.total_pings_sent += 1
        
        if self.consecutive_failures >= max_failures:
            self.is_healthy = False

<strong># Keepalive configuration
</strong>@dataclass
class KeepaliveConfig:
    ping_interval: float = 60.0  # seconds
    max_consecutive_failures: int = 3
    reconnect_delay: float = 5.0  # seconds
    health_check_enabled: bool = True
    timeout: float = 15.0  # seconds

<strong># Basic keepalive implementation
</strong>async def start_keepalive_task(
    # nextblock_client,  # Your generated gRPC client
    config: KeepaliveConfig = None,
) -> None:
<strong>    """Start basic keepalive task"""
</strong>    if config is None:
        config = KeepaliveConfig()
    
    print(f"Starting keepalive task with {config.ping_interval}s interval")
    
    while True:
        try:
            await asyncio.sleep(config.ping_interval)
            
<strong>            # Send ping request
</strong>            start_time = time.time()
            
            """ Uncomment when you have the generated gRPC client
            try:
                await asyncio.wait_for(
                    nextblock_client.ping(Empty()),
                    timeout=config.timeout
                )
                
                ping_time = time.time() - start_time
                print(f"Keepalive ping successful ({ping_time*1000:.1f}ms) at {datetime.now().strftime('%H:%M:%S')}")
                
            except asyncio.TimeoutError:
                print(f"Keepalive ping timeout after {config.timeout}s")
            except Exception as e:
                print(f"Keepalive ping failed: {e}")
                # Optionally implement reconnection logic
                break
            """
            
<strong>            # Mock ping for demonstration
</strong>            await asyncio.sleep(0.1)  # Simulate network delay
            ping_time = time.time() - start_time
            print(f"Mock keepalive ping successful ({ping_time*1000:.1f}ms) at {datetime.now().strftime('%H:%M:%S')}")
            
        except Exception as e:
            logging.error(f"Keepalive task error: {e}")
            break

<strong># Advanced keepalive manager
</strong>class KeepaliveManager:
    def __init__(
        self,
        # nextblock_client,  # Your generated gRPC client
        config: KeepaliveConfig = None,
    ):
        # self.client = nextblock_client
        self.config = config or KeepaliveConfig()
        self.health = ConnectionHealth()
        self.keepalive_task: Optional[asyncio.Task] = None
        self.is_running = False
    
    async def start(self) -> None:
<strong>        """Start the keepalive manager"""
</strong>        if self.is_running:
            return
        
        self.is_running = True
        self.keepalive_task = asyncio.create_task(self._keepalive_loop())
        print("Keepalive manager started")
    
    async def stop(self) -> None:
<strong>        """Stop the keepalive manager"""
</strong>        self.is_running = False
        
        if self.keepalive_task and not self.keepalive_task.done():
            self.keepalive_task.cancel()
            try:
                await self.keepalive_task
            except asyncio.CancelledError:
                pass
        
        print("Keepalive manager stopped")
    
    async def _keepalive_loop(self) -> None:
<strong>        """Main keepalive loop"""
</strong>        while self.is_running:
            try:
                await asyncio.sleep(self.config.ping_interval)
                await self._send_ping()
                
            except asyncio.CancelledError:
                break
            except Exception as e:
                logging.error(f"Keepalive loop error: {e}")
                
<strong>                # Handle connection recovery
</strong>                if not self.health.is_healthy:
                    await self._handle_connection_recovery()
    
    async def _send_ping(self) -> None:
<strong>        """Send a single ping and update health"""
</strong>        start_time = time.time()
        
        try:
            """ Uncomment when you have the generated gRPC client
            await asyncio.wait_for(
                self.client.ping(Empty()),
                timeout=self.config.timeout
            )
            """
            
<strong>            # Mock ping delay
</strong>            await asyncio.sleep(0.05 + (time.time() % 0.1))  # Variable delay 50-150ms
            
            ping_time = time.time() - start_time
            self.health.update_success(ping_time)
            
            print(f"Keepalive ping successful ({ping_time*1000:.1f}ms) - "
                  f"Health: {self.health.success_rate():.1f}%")
            
        except asyncio.TimeoutError:
            self.health.update_failure(self.config.max_consecutive_failures)
            logging.warning(f"Keepalive ping timeout after {self.config.timeout}s")
            
        except Exception as e:
            self.health.update_failure(self.config.max_consecutive_failures)
            logging.error(f"Keepalive ping failed: {e}")
    
    async def _handle_connection_recovery(self) -> None:
<strong>        """Handle connection recovery when unhealthy"""
</strong>        logging.warning("Connection unhealthy, attempting recovery...")
        
<strong>        # Wait before attempting recovery
</strong>        await asyncio.sleep(self.config.reconnect_delay)
        
<strong>        # Try to recover connection
</strong>        try:
<strong>            # Implement connection recovery logic here
</strong>            # await self._reconnect()
            print("Connection recovery attempted")
            
        except Exception as e:
            logging.error(f"Connection recovery failed: {e}")
    
    def get_health(self) -> ConnectionHealth:
<strong>        """Get current connection health"""
</strong>        return self.health
    
    def is_healthy(self) -> bool:
<strong>        """Check if connection is healthy"""
</strong>        return self.health.is_healthy

<strong># Connection manager with integrated keepalive
</strong>class ConnectionManagerWithKeepalive:
    def __init__(self, nextblock_config, keepalive_config: KeepaliveConfig = None):
        self.nextblock_config = nextblock_config
        self.keepalive_config = keepalive_config or KeepaliveConfig()
        # self.client = None
        self.keepalive_manager: Optional[KeepaliveManager] = None
        self.is_connected = False
    
    async def connect(self) -> bool:
<strong>        """Establish connection and start keepalive"""
</strong>        try:
<strong>            # Create connection (see connection.md)
</strong>            # channel, client = await create_nextblock_client(self.nextblock_config)
            # self.client = client
            
<strong>            # Test connection
</strong>            # await self.client.ping(Empty())
            
            self.is_connected = True
            print("Successfully connected to NextBlock")
            
<strong>            # Start keepalive
</strong>            self.keepalive_manager = KeepaliveManager(
                # self.client,
                self.keepalive_config
            )
            await self.keepalive_manager.start()
            
            return True
            
        except Exception as e:
            logging.error(f"Failed to connect: {e}")
            self.is_connected = False
            return False
    
    async def disconnect(self) -> None:
<strong>        """Disconnect and stop keepalive"""
</strong>        if self.keepalive_manager:
            await self.keepalive_manager.stop()
        
        self.is_connected = False
        print("Disconnected from NextBlock")
    
    def get_connection_health(self) -> Optional[ConnectionHealth]:
<strong>        """Get current connection health"""
</strong>        if self.keepalive_manager:
            return self.keepalive_manager.get_health()
        return None
    
    async def __aenter__(self):
        await self.connect()
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.disconnect()

<strong># Health monitoring and alerting
</strong>class HealthMonitor:
    def __init__(self, keepalive_manager: KeepaliveManager):
        self.keepalive_manager = keepalive_manager
        self.alert_thresholds = {
            "success_rate": 90.0,  # Alert if success rate &#x3C; 90%
            "avg_ping_time": 1.0,  # Alert if avg ping time > 1s
            "consecutive_failures": 2,  # Alert after 2 consecutive failures
        }
    
    async def start_monitoring(self, check_interval: float = 30.0) -> None:
<strong>        """Start health monitoring with periodic checks"""
</strong>        while True:
            await asyncio.sleep(check_interval)
            await self._check_health()
    
    async def _check_health(self) -> None:
<strong>        """Check connection health and trigger alerts if needed"""
</strong>        health = self.keepalive_manager.get_health()
        
<strong>        # Check success rate
</strong>        if health.success_rate() &#x3C; self.alert_thresholds["success_rate"]:
            await self._trigger_alert(
                "Low success rate",
                f"Success rate: {health.success_rate():.1f}%"
            )
        
<strong>        # Check average ping time
</strong>        if health.average_ping_time > self.alert_thresholds["avg_ping_time"]:
            await self._trigger_alert(
                "High ping time",
                f"Average ping time: {health.average_ping_time*1000:.1f}ms"
            )
        
<strong>        # Check consecutive failures
</strong>        if health.consecutive_failures >= self.alert_thresholds["consecutive_failures"]:
            await self._trigger_alert(
                "Connection issues",
                f"Consecutive failures: {health.consecutive_failures}"
            )
    
    async def _trigger_alert(self, alert_type: str, details: str) -> None:
<strong>        """Trigger health alert"""
</strong>        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"HEALTH ALERT [{timestamp}] {alert_type}: {details}")
        
<strong>        # Implement additional alerting logic here
</strong>        # - Send email notifications
        # - Post to Slack/Discord
        # - Write to monitoring system
</code></pre>

## Usage Examples

<pre class="language-python"><code class="lang-python"><strong>async def basic_keepalive_example():
</strong><strong>    """Basic keepalive usage"""
</strong>    # Connect to NextBlock (see connection.md)
    # config = NextBlockConfig.from_env()
    # channel, client = await create_nextblock_client(config)
    
<strong>    # Start keepalive task
</strong>    keepalive_task = asyncio.create_task(
        start_keepalive_task(
            # client,
            KeepaliveConfig(ping_interval=60.0)
        )
    )
    
<strong>    # Your main application logic here
</strong>    print("Application running with keepalive...")
    
    try:
<strong>        # Simulate application work
</strong>        await asyncio.sleep(300)  # Run for 5 minutes
    except KeyboardInterrupt:
        print("Stopping application...")
    finally:
        keepalive_task.cancel()
        try:
            await keepalive_task
        except asyncio.CancelledError:
            pass

<strong>async def advanced_keepalive_example():
</strong><strong>    """Advanced keepalive with health monitoring"""
</strong>    # from connection import NextBlockConfig
    
<strong>    # Configuration
</strong>    # nextblock_config = NextBlockConfig.from_env()
    keepalive_config = KeepaliveConfig(
        ping_interval=30.0,  # Ping every 30 seconds
        max_consecutive_failures=3,
        reconnect_delay=10.0,
        timeout=15.0,
    )
    
<strong>    # Use connection manager with integrated keepalive
</strong>    async with ConnectionManagerWithKeepalive(
        # nextblock_config,
        None,  # Placeholder
        keepalive_config
    ) as manager:
        
        if manager.is_connected:
            print("Connected with keepalive enabled")
            
<strong>            # Start health monitoring
</strong>            health_monitor = HealthMonitor(manager.keepalive_manager)
            monitor_task = asyncio.create_task(
                health_monitor.start_monitoring(check_interval=60.0)
            )
            
            try:
<strong>                # Your application logic here
</strong>                for i in range(10):
                    await asyncio.sleep(30)
                    
                    health = manager.get_connection_health()
                    if health:
                        print(f"Connection health check {i+1}:")
                        print(f"  Healthy: {health.is_healthy}")
                        print(f"  Success rate: {health.success_rate():.1f}%")
                        print(f"  Avg ping time: {health.average_ping_time*1000:.1f}ms")
                        print(f"  Total pings: {health.total_pings_sent}")
                
            except KeyboardInterrupt:
                print("Application interrupted")
            finally:
                monitor_task.cancel()
                try:
                    await monitor_task
                except asyncio.CancelledError:
                    pass

<strong>async def main():
</strong><strong>    """Main example runner"""
</strong>    print("Choose example:")
    print("1. Basic keepalive")
    print("2. Advanced keepalive with monitoring")
    
    choice = input("Enter choice (1 or 2): ").strip()
    
    if choice == "1":
        await basic_keepalive_example()
    elif choice == "2":
        await advanced_keepalive_example()
    else:
        print("Invalid choice")

if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s'
    )
    asyncio.run(main())
</code></pre>

## Best Practices

1. **Appropriate intervals**: Use 30-60 second ping intervals for most applications
2. **Health monitoring**: Track connection health and implement alerting
3. **Graceful recovery**: Handle connection failures with exponential backoff
4. **Resource cleanup**: Always stop keepalive tasks when shutting down
5. **Timeout handling**: Set reasonable timeouts for ping requests
6. **Logging**: Log keepalive events for debugging and monitoring
7. **Integration**: Integrate keepalive with your connection management system


# JavaScript/TypeScript

Complete JavaScript and TypeScript examples for integrating with NextBlock's gRPC API and QUIC transaction submission.

## Overview

These examples demonstrate how to:

* Establish secure gRPC connections with authentication
* Submit raw transaction bytes over QUIC for lower transport overhead
* Submit single and batched transactions with proper tipping
* Stream real-time tip floor data for dynamic tip optimization
* Maintain persistent connections with keepalive mechanisms

## Prerequisites

Install the required dependencies:

```bash
npm install @grpc/grpc-js @grpc/proto-loader
npm install @solana/web3.js @solana/spl-token
npm install typescript @types/node  # For TypeScript support

# Generate JavaScript gRPC client from nextblock-proto
# See https://github.com/nextblock-ag/nextblock-proto for instructions
```

## Examples

### Core Examples

* [Connection](/api/examples/javascript/connection) - Establish gRPC connections with authentication
* [QUIC Transaction Submission](/api/examples/javascript/quic) - Send raw signed transaction bytes over QUIC
* [Submit Single Transaction](/api/examples/javascript/submit-single-transactions) - Send individual transactions with tips
* [Submit Batched Transactions](/api/examples/javascript/submit-batched-transactions) - Send atomic transaction bundles
* [Tip Floor Stream](/api/examples/javascript/tip-floor-stream) - Stream real-time tip floor data
* [Keepalive](/api/examples/javascript/keepalive) - Maintain persistent connections

## Quick Start

1. **Generate gRPC client** from [nextblock-proto](https://github.com/nextblock-ag/nextblock-proto)
2. **Install dependencies** using npm/yarn
3. **Set up environment** with API key and endpoint
4. **Start with connection example** to establish authenticated gRPC connection
5. **Use tip floor streaming** to optimize transaction tips dynamically
6. **Submit transactions** using single or batched submission methods

## Key Features

### TypeScript Support

All examples include full TypeScript type definitions for better development experience.

### Modern JavaScript

Uses modern ES6+ features including async/await, destructuring, and modules.

### Error Handling

Comprehensive error handling with retry logic and exponential backoff.

### Connection Management

Persistent connections with keepalive, health monitoring, and automatic recovery.

### Dynamic Tipping

Real-time tip optimization based on current network conditions from tip floor API.

## Best Practices

1. **Use TypeScript** - Leverage type safety for better code quality
2. **Enable TLS** - Always use secure connections in production
3. **Implement keepalive** - Maintain persistent connections for better performance
4. **Monitor tip floors** - Use streaming API for dynamic tip adjustment
5. **Handle errors gracefully** - Implement proper retry logic with exponential backoff
6. **Use environment variables** - Store sensitive configuration securely
7. **Choose appropriate endpoints** - Use the closest endpoint for better latency
8. **Bundle transactions** - Use batched submissions for atomic operations


# Connection

Establish a gRPC connection to NextBlock's API using Node.js and `@grpc/grpc-js`.

This page shows the connection pattern and authentication interceptor. Replace the placeholder generated client wiring with the client generated from [`nextblock-proto`](https://github.com/nextblock-ag/nextblock-proto).

## Prerequisites

Install the required dependencies:

```bash
npm install @grpc/grpc-js @grpc/proto-loader
npm install @solana/web3.js
npm install typescript @types/node  # For TypeScript
```

Generate the gRPC client from proto specs:

```bash
# Clone the proto repository
git clone https://github.com/nextblock-ag/nextblock-proto
# Follow the JavaScript/TypeScript generation instructions in the repo
```

## Connection Setup

<pre class="language-typescript"><code class="lang-typescript"><strong>import * as grpc from '@grpc/grpc-js';
</strong>import * as protoLoader from '@grpc/proto-loader';
import { promisify } from 'util';

<strong>// Configuration interface
</strong>interface NextBlockConfig {
  endpoint: string;
  apiKey: string;
  useTLS: boolean;
  timeout: number;
  keepaliveTimeMs: number;
  keepaliveTimeoutMs: number;
}

<strong>// Default configuration
</strong>const defaultConfig: NextBlockConfig = {
  endpoint: 'frankfurt.nextblock.io:443',
  apiKey: process.env.NEXTBLOCK_API_KEY || '',
  useTLS: true,
  timeout: 30000,
  keepaliveTimeMs: 60000,  // 60 seconds
  keepaliveTimeoutMs: 15000, // 15 seconds
};

<strong>// Authentication metadata interceptor
</strong>class AuthInterceptor {
  private apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

<strong>  // Add authorization header to all requests
</strong>  intercept(options: any, nextCall: any) {
    return new grpc.InterceptingCall(nextCall(options), {
      start: (metadata: grpc.Metadata, listener: any, next: any) => {
        metadata.set('authorization', this.apiKey);
        next(metadata, listener);
      },
    });
  }
}

<strong>// Create gRPC channel with authentication
</strong>export async function createNextBlockChannel(config: NextBlockConfig): Promise&#x3C;grpc.Channel> {
<strong>  // Channel options for keepalive and performance
</strong>  const channelOptions: grpc.ChannelOptions = {
    'grpc.keepalive_time_ms': config.keepaliveTimeMs,
    'grpc.keepalive_timeout_ms': config.keepaliveTimeoutMs,
    'grpc.keepalive_permit_without_stream': 1,
    'grpc.http2.max_pings_without_data': 0,
    'grpc.http2.min_ping_interval_without_data_ms': 300000, // 5 minutes
  };

<strong>  // Create credentials
</strong>  const credentials = config.useTLS 
    ? grpc.credentials.createSsl()
    : grpc.credentials.createInsecure();

<strong>  // Create channel
</strong>  const channel = new grpc.Channel(config.endpoint, credentials, channelOptions);
  
  return channel;
}

<strong>// Create authenticated gRPC client
</strong>export async function createNextBlockClient(config: NextBlockConfig = defaultConfig) {
<strong>  // Validate configuration
</strong>  if (!config.apiKey) {
    throw new Error('API key is required');
  }

<strong>  // Create channel
</strong>  const channel = await createNextBlockChannel(config);

<strong>  // Create authentication interceptor
</strong>  const authInterceptor = new AuthInterceptor(config.apiKey);

<strong>  // Load proto definition (replace with your generated client)
</strong>  /* Example proto loading - replace with your actual proto
  const packageDefinition = protoLoader.loadSync('path/to/your/nextblock.proto', {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
  });

  const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
  const ApiClient = protoDescriptor.nextblock.Api;

  // Create client with interceptor
  const client = new ApiClient(config.endpoint, credentials, {
    interceptors: [authInterceptor.intercept.bind(authInterceptor)],
    ...channelOptions,
  });
  */

  console.log(`Connected to NextBlock at ${config.endpoint}`);
  
  return {
    channel,
    // client, // Uncomment when you have the generated client
    config,
  };
}

<strong>// Connection manager with health checking
</strong>export class NextBlockConnectionManager {
  private config: NextBlockConfig;
  private channel?: grpc.Channel;
  private client?: any; // Replace with your generated client type
  private isConnected: boolean = false;

  constructor(config: NextBlockConfig = defaultConfig) {
    this.config = config;
  }

<strong>  // Establish connection
</strong>  async connect(): Promise&#x3C;boolean> {
    try {
      const connection = await createNextBlockClient(this.config);
      this.channel = connection.channel;
      // this.client = connection.client;

<strong>      // Test the connection
</strong>      await this.healthCheck();
      this.isConnected = true;
      console.log(`Successfully connected to NextBlock at ${this.config.endpoint}`);
      return true;

    } catch (error) {
      console.error('Failed to connect to NextBlock:', error);
      this.isConnected = false;
      return false;
    }
  }

<strong>  // Health check
</strong>  async healthCheck(): Promise&#x3C;boolean> {
    if (!this.channel) {
      return false;
    }

    return new Promise((resolve) => {
<strong>      // Check channel state
</strong>      const state = this.channel!.getConnectivityState(false);
      
      if (state === grpc.connectivityState.READY) {
        console.log('Connection health check passed');
        resolve(true);
      } else {
        console.log(`Connection state: ${grpc.connectivityState[state]}`);
        resolve(false);
      }
    });
  }

<strong>  // Disconnect
</strong>  async disconnect(): Promise&#x3C;void> {
    if (this.channel) {
      this.channel.close();
      this.isConnected = false;
      console.log('Disconnected from NextBlock');
    }
  }

<strong>  // Getters
</strong>  get connected(): boolean {
    return this.isConnected;
  }

  get grpcClient(): any {
    return this.client;
  }
}

<strong>// Configuration from environment variables
</strong>export function configFromEnv(): NextBlockConfig {
  return {
    endpoint: process.env.NEXTBLOCK_ENDPOINT || 'frankfurt.nextblock.io:443',
    apiKey: process.env.NEXTBLOCK_API_KEY || '',
    useTLS: process.env.NEXTBLOCK_USE_TLS !== 'false',
    timeout: parseInt(process.env.NEXTBLOCK_TIMEOUT || '30000'),
    keepaliveTimeMs: parseInt(process.env.NEXTBLOCK_KEEPALIVE_TIME_MS || '60000'),
    keepaliveTimeoutMs: parseInt(process.env.NEXTBLOCK_KEEPALIVE_TIMEOUT_MS || '15000'),
  };
}
</code></pre>

## Usage Examples

<pre class="language-typescript"><code class="lang-typescript"><strong>// Basic connection example
</strong>async function basicConnectionExample() {
  const config: NextBlockConfig = {
    endpoint: 'frankfurt.nextblock.io:443',
    apiKey: '&#x3C;your-api-key-here>',
    useTLS: true,
    timeout: 30000,
    keepaliveTimeMs: 60000,
    keepaliveTimeoutMs: 15000,
  };

  try {
    const connection = await createNextBlockClient(config);
    console.log('Connected to NextBlock!');

<strong>    // Your API calls would go here
</strong>    // const response = await connection.client.ping({});
    
<strong>    // Clean up
</strong>    connection.channel.close();
    
  } catch (error) {
    console.error('Connection failed:', error);
  }
}

<strong>// Connection manager example (recommended)
</strong>async function connectionManagerExample() {
  const config = configFromEnv();
  const manager = new NextBlockConnectionManager(config);

  try {
    const connected = await manager.connect();
    
    if (connected) {
      console.log('Connection manager established connection');
      
<strong>      // Use manager.grpcClient for API calls
</strong>      // const response = await manager.grpcClient.ping({});
      
<strong>      // Keep connection alive for your application
</strong>      await new Promise(resolve => setTimeout(resolve, 5000));
      
    } else {
      console.error('Failed to establish connection');
    }
    
  } finally {
    await manager.disconnect();
  }
}

<strong>// Advanced connection with retry logic
</strong>class EnhancedConnectionManager extends NextBlockConnectionManager {
  private maxRetries: number = 3;
  private retryDelay: number = 5000; // 5 seconds

  async connectWithRetry(): Promise&#x3C;boolean> {
    for (let attempt = 1; attempt &#x3C;= this.maxRetries; attempt++) {
      console.log(`Connection attempt ${attempt}/${this.maxRetries}`);
      
      const success = await this.connect();
      if (success) {
        return true;
      }

      if (attempt &#x3C; this.maxRetries) {
        console.log(`Retrying in ${this.retryDelay}ms...`);
        await new Promise(resolve => setTimeout(resolve, this.retryDelay));
        this.retryDelay *= 2; // Exponential backoff
      }
    }

    console.error('All connection attempts failed');
    return false;
  }

  async callWithRetry&#x3C;T>(
    fn: () => Promise&#x3C;T>,
    maxRetries: number = 3
  ): Promise&#x3C;T> {
    let lastError: Error;

    for (let attempt = 1; attempt &#x3C;= maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error as Error;
        
<strong>        // Don't retry on authentication errors
</strong>        if (error &#x26;&#x26; (error as any).code === grpc.status.UNAUTHENTICATED) {
          throw error;
        }

        if (attempt &#x3C; maxRetries) {
          const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000);
          console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError!;
  }
}

<strong>// Connection pool for high-throughput applications
</strong>class ConnectionPool {
  private connections: NextBlockConnectionManager[] = [];
  private currentIndex: number = 0;
  private config: NextBlockConfig;
  private poolSize: number;

  constructor(config: NextBlockConfig, poolSize: number = 5) {
    this.config = config;
    this.poolSize = poolSize;
  }

  async initialize(): Promise&#x3C;void> {
    console.log(`Initializing connection pool with ${this.poolSize} connections...`);
    
    const connectionPromises = Array.from({ length: this.poolSize }, async (_, i) => {
      const manager = new NextBlockConnectionManager(this.config);
      const connected = await manager.connect();
      
      if (connected) {
        this.connections.push(manager);
        console.log(`Connection ${i + 1} established`);
      } else {
        console.warn(`Failed to create connection ${i + 1}`);
      }
    });

    await Promise.all(connectionPromises);
    console.log(`Connection pool initialized with ${this.connections.length} connections`);
  }

  getConnection(): NextBlockConnectionManager {
    if (this.connections.length === 0) {
      throw new Error('No available connections in pool');
    }

    const connection = this.connections[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.connections.length;
    return connection;
  }

  async closeAll(): Promise&#x3C;void> {
    await Promise.all(this.connections.map(conn => conn.disconnect()));
    this.connections = [];
    console.log('All connections closed');
  }
}

<strong>// Run examples
</strong>async function main() {
  console.log('NextBlock Connection Examples');
  
<strong>  // Example 1: Basic connection
</strong>  console.log('\n1. Basic Connection:');
  await basicConnectionExample();
  
<strong>  // Example 2: Connection manager
</strong>  console.log('\n2. Connection Manager:');
  await connectionManagerExample();
  
<strong>  // Example 3: Enhanced connection with retry
</strong>  console.log('\n3. Enhanced Connection:');
  const enhancedManager = new EnhancedConnectionManager(configFromEnv());
  const connected = await enhancedManager.connectWithRetry();
  if (connected) {
    console.log('Enhanced connection successful');
    await enhancedManager.disconnect();
  }
}

<strong>// Export for use in other modules
</strong>export {
  NextBlockConfig,
  NextBlockConnectionManager,
  EnhancedConnectionManager,
  ConnectionPool,
  configFromEnv,
};

if (require.main === module) {
  main().catch(console.error);
}
</code></pre>

## Available Endpoints

* **Frankfurt**: `frankfurt.nextblock.io:443` (Europe)
* **Amsterdam**: `amsterdam.nextblock.io:443` (Europe)
* **London**: `london.nextblock.io:443` (Europe)
* **Singapore**: `singapore.nextblock.io:443` (Asia)
* **Tokyo**: `tokyo.nextblock.io:443` (Asia)
* **New York**: `ny.nextblock.io:443` (US East)
* **Salt Lake City**: `slc.nextblock.io:443` (US West)
* **Dublin**: `dublin.nextblock.io:443` (Europe)
* **Vilnius**: `vilnius.nextblock.io:443` (Europe)

## Best Practices

1. **Use TypeScript**: Leverage type safety for better development experience
2. **Enable TLS**: Always use secure connections in production
3. **Implement keepalive**: Configure appropriate keepalive settings for persistent connections
4. **Handle errors gracefully**: Use retry logic with exponential backoff
5. **Validate configuration**: Check all required settings before connecting
6. **Use connection pooling**: For high-throughput applications
7. **Monitor connection health**: Implement regular health checks
8. **Environment variables**: Store sensitive configuration securely
9. **Close connections properly**: Always close connections when done
10. **Use interceptors**: Implement authentication and logging via gRPC interceptors


# QUIC Transaction Submission

Use QUIC when you already have signed Solana transaction bytes and want a low-overhead submission path from Node.js or TypeScript.

This example uses [`@matrixai/quic`](https://www.npmjs.com/package/@matrixai/quic) and follows the same flow as the other languages:

1. Connect to a regional endpoint with ALPN `nb-tx/1`
2. Authenticate once with your API key on a bidirectional stream
3. Reuse the connection and send each transaction on a unidirectional stream

## Example

```typescript
import { randomFillSync } from "node:crypto";
import { QUICClient } from "@matrixai/quic";

const AUTH_OK = 0x00;
const MAX_TX_SIZE = 1232;

class NextblockQuicClient {
  constructor(private readonly client: QUICClient) {}

  static async connect(serverAddr: string, apiKey: string): Promise<NextblockQuicClient> {
    const [host, portString] = serverAddr.split(":");
    const port = Number(portString);

    const client = await QUICClient.createQUICClient({
      host,
      port,
      serverName: host,
      crypto: {
        ops: {
          async randomBytes(data: ArrayBuffer): Promise<void> {
            randomFillSync(new Uint8Array(data));
          },
        },
      },
      config: {
        applicationProtos: ["nb-tx/1"],
        maxIdleTimeout: 60_000,
        keepAliveIntervalTime: 15_000,
      },
    });

    const authStream = client.connection.newStream("bidi");
    const authWriter = authStream.writable.getWriter();
    await authWriter.write(new TextEncoder().encode(apiKey));
    await authWriter.close();

    const authReader = authStream.readable.getReader();
    const { value } = await authReader.read();
    if (!value || value[0] !== AUTH_OK) {
      await client.destroy();
      throw new Error("Authentication rejected");
    }

    return new NextblockQuicClient(client);
  }

  async sendTransaction(rawTx: Uint8Array): Promise<void> {
    if (rawTx.byteLength > MAX_TX_SIZE) {
      throw new Error(`Transaction too large: ${rawTx.byteLength}`);
    }

    const txStream = this.client.connection.newStream("uni");
    const txWriter = txStream.writable.getWriter();
    await txWriter.write(rawTx);
    await txWriter.close();
  }

  async close(): Promise<void> {
    await this.client.destroy();
  }
}

async function main(): Promise<void> {
  const apiKey = process.env.NEXTBLOCK_API_KEY;
  if (!apiKey) {
    throw new Error("Set NEXTBLOCK_API_KEY before running");
  }

  const client = await NextblockQuicClient.connect(
    "amsterdam.nextblock.io:11100",
    apiKey,
  );

  try {
    // Replace this with the serialized bytes of your signed transaction.
    // Example with @solana/web3.js: const rawTx = signedTransaction.serialize();
    const rawTx = new Uint8Array([0, 1, 2, 3]);
    await client.sendTransaction(rawTx);
    console.log("transaction queued");
  } finally {
    await client.close();
  }
}

void main();
```

## What To Replace

* Replace `amsterdam.nextblock.io:11100` with the region closest to you.
* Read the API key from `NEXTBLOCK_API_KEY` or your own config source.
* Replace `rawTx` with the serialized bytes of your signed transaction.

## Notes

* Send raw transaction bytes, not base64.
* Reuse one `NextblockQuicClient` for many sends.
* QUIC does not support the extra gRPC submission flags or atomic bundle submission.
* If you build transactions with `@solana/web3.js`, serialize the signed transaction first and pass the resulting `Uint8Array` into `sendTransaction()`.

See [QUIC Transaction Submission](/api/quic-transaction-submission) for the full endpoint list and protocol summary.


# Submit Single Transaction

Submit individual transactions to NextBlock using JavaScript/TypeScript with proper tipping and error handling.

If you want to send raw signed transaction bytes over QUIC instead of gRPC, see [QUIC Transaction Submission](/api/examples/javascript/quic).

This example shows transaction construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

<pre class="language-typescript"><code class="lang-typescript"><strong>import { 
</strong>  Connection, 
  Keypair, 
  PublicKey, 
  SystemProgram, 
  Transaction, 
  sendAndConfirmTransaction 
} from '@solana/web3.js';

<strong>// NextBlock tip wallets for load balancing
</strong>const NEXTBLOCK_TIP_WALLETS = [
  'NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid',
  'nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc',
  'NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE',
  'NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2',
  'NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X',
  'NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb',
  'neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At',
  'nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG',
];

<strong>// Submission options interface
</strong>interface SubmissionOptions {
  skipPreflight?: boolean;
  frontRunningProtection?: boolean;
  revertOnFail?: boolean;
  disableRetries?: boolean;
  snipeTransaction?: boolean;
}

<strong>// Get random tip wallet for load balancing
</strong>function getRandomNextblockTipWallet(): PublicKey {
  const randomIndex = Math.floor(Math.random() * NEXTBLOCK_TIP_WALLETS.length);
  return new PublicKey(NEXTBLOCK_TIP_WALLETS[randomIndex]);
}

<strong>// Build transaction with tip
</strong>async function buildTransactionWithTip(
  connection: Connection,
  payer: Keypair,
  tipAmount: number,
  instructions: any[]
): Promise&#x3C;Transaction> {
<strong>  // Get recent blockhash
</strong>  const { blockhash } = await connection.getLatestBlockhash('finalized');
  
<strong>  // Create tip instruction (should be first)
</strong>  const tipWallet = getRandomNextblockTipWallet();
  const tipInstruction = SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: tipWallet,
    lamports: tipAmount,
  });
  
<strong>  // Create transaction with tip and user instructions
</strong>  const transaction = new Transaction({
    recentBlockhash: blockhash,
    feePayer: payer.publicKey,
  });
  
  transaction.add(tipInstruction);
  instructions.forEach(instruction => transaction.add(instruction));
  
<strong>  // Sign transaction
</strong>  transaction.sign(payer);
  
  return transaction;
}

<strong>// Submit single transaction to NextBlock
</strong>async function submitSingleTransaction(
  // nextblockClient: any, // Your generated gRPC client
  connection: Connection,
  signer: Keypair,
  recipient: PublicKey,
  transferAmount: number,
  tipAmount: number,
  options: SubmissionOptions = {}
): Promise&#x3C;string> {
<strong>  // Default options
</strong>  const opts: SubmissionOptions = {
    skipPreflight: true,
    frontRunningProtection: false,
    revertOnFail: false,
    disableRetries: false,
    snipeTransaction: false,
    ...options,
  };
  
<strong>  // Create transfer instruction
</strong>  const transferInstruction = SystemProgram.transfer({
    fromPubkey: signer.publicKey,
    toPubkey: recipient,
    lamports: transferAmount,
  });
  
<strong>  // Build transaction with tip
</strong>  const transaction = await buildTransactionWithTip(
    connection,
    signer,
    tipAmount,
    [transferInstruction]
  );
  
<strong>  // Convert to base64 for submission
</strong>  const serializedTx = transaction.serialize();
  const base64Tx = serializedTx.toString('base64');
  
<strong>  // Submit to NextBlock
</strong>  /* Uncomment when you have the generated gRPC client
  const request = {
    transaction: { content: base64Tx },
    skipPreFlight: opts.skipPreflight,
    snipeTransaction: opts.snipeTransaction,
    frontRunningProtection: opts.frontRunningProtection,
    disableRetries: opts.disableRetries,
    revertOnFail: opts.revertOnFail,
  };
  
  const response = await nextblockClient.postSubmitV2(request);
  
  console.log('Transaction submitted successfully!');
  console.log(`Signature: ${response.signature}`);
  console.log(`UUID: ${response.uuid}`);
  
  return response.signature;
  */
  
<strong>  // Until your generated client is wired in, return the local signature
</strong>  const signature = transaction.signature?.toString() || '';
  console.log(`Local transaction built successfully: ${signature}`);
  return signature;
}

<strong>// Submit with priority-based tip calculation
</strong>async function submitWithOptimalTip(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair,
  recipient: PublicKey,
  transferAmount: number,
  priorityLevel: 'conservative' | 'normal' | 'aggressive' | 'priority' = 'normal'
): Promise&#x3C;string> {
<strong>  // Tip amounts based on priority level
</strong>  // Get tip amounts from current tip floor data
  // Higher tips = higher priority
  // Use tip floor streaming API to get current optimal tips
  const tipAmount = await getOptimalTipFromTipFloor(priorityLevel);
  

  
  return await submitSingleTransaction(
    // nextblockClient,
    connection,
    signer,
    recipient,
    transferAmount,
    tipAmount
  );
}

<strong>// Transaction builder class for complex transactions
</strong>class TransactionBuilder {
  private payer: Keypair;
  private instructions: any[] = [];
  private tipAmount: number = 1_000_000; // Default tip

  constructor(payer: Keypair) {
    this.payer = payer;
  }

  addInstruction(instruction: any): TransactionBuilder {
    this.instructions.push(instruction);
    return this;
  }

  setTipAmount(amount: number): TransactionBuilder {
    this.tipAmount = amount;
    return this;
  }

  async buildAndSubmit(
    // nextblockClient: any,
    connection: Connection,
    options?: SubmissionOptions
  ): Promise&#x3C;string> {
    if (this.instructions.length === 0) {
      throw new Error('No instructions added to transaction');
    }

    const transaction = await buildTransactionWithTip(
      connection,
      this.payer,
      this.tipAmount,
      this.instructions
    );

<strong>    // Convert and submit (similar to submitSingleTransaction)
</strong>    const serializedTx = transaction.serialize();
    const base64Tx = serializedTx.toString('base64');

    const signature = transaction.signature?.toString() || '';
    console.log(`Custom transaction built locally: ${signature}`);
    return signature;
  }
}

<strong>// Batch multiple single transactions (concurrent submission)
</strong>async function submitMultipleSingleTransactions(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair,
  transactionsData: Array&#x3C;{
    recipient: PublicKey;
    transferAmount: number;
    tipAmount: number;
  }>
): Promise&#x3C;string[]> {
<strong>  // Create promises for all transactions
</strong>  const transactionPromises = transactionsData.map(async ({ recipient, transferAmount, tipAmount }) => {
    try {
      return await submitSingleTransaction(
        // nextblockClient,
        connection,
        signer,
        recipient,
        transferAmount,
        tipAmount
      );
    } catch (error) {
      console.error(`Transaction failed:`, error);
      return null;
    }
  });

<strong>  // Execute all transactions concurrently
</strong>  const results = await Promise.allSettled(transactionPromises);
  
<strong>  // Process results
</strong>  const successfulSignatures: string[] = [];
  results.forEach((result, index) => {
    if (result.status === 'fulfilled' &#x26;&#x26; result.value) {
      successfulSignatures.push(result.value);
      console.log(`Transaction ${index + 1} successful: ${result.value}`);
    } else {
      console.error(`Transaction ${index + 1} failed:`, result);
    }
  });

  return successfulSignatures;
}

<strong>// Performance monitoring
</strong>class TransactionMetrics {
  private submissions: number = 0;
  private successes: number = 0;
  private failures: number = 0;
  private totalTime: number = 0;
  private startTime?: number;

  startSubmission(): void {
    this.submissions++;
    this.startTime = Date.now();
  }

  recordSuccess(): void {
    if (this.startTime) {
      this.totalTime += Date.now() - this.startTime;
      this.successes++;
      this.startTime = undefined;
    }
  }

  recordFailure(): void {
    if (this.startTime) {
      this.totalTime += Date.now() - this.startTime;
      this.failures++;
      this.startTime = undefined;
    }
  }

  getStats() {
    return {
      totalSubmissions: this.submissions,
      successes: this.successes,
      failures: this.failures,
      successRate: (this.successes / Math.max(this.submissions, 1)) * 100,
      averageTime: this.totalTime / Math.max(this.successes, 1),
    };
  }
}

<strong>// Submit with monitoring
</strong>async function submitWithMonitoring(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair,
  recipient: PublicKey,
  transferAmount: number,
  tipAmount: number,
  metrics: TransactionMetrics
): Promise&#x3C;string | null> {
  metrics.startSubmission();

  try {
    const signature = await submitSingleTransaction(
      // nextblockClient,
      connection,
      signer,
      recipient,
      transferAmount,
      tipAmount
    );
    metrics.recordSuccess();
    return signature;
  } catch (error) {
    metrics.recordFailure();
    console.error('Transaction failed:', error);
    return null;
  }
}
</code></pre>

## Usage Examples

<pre class="language-typescript"><code class="lang-typescript"><strong>async function main() {
</strong><strong>  // Initialize Solana connection
</strong>  const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
  const signer = Keypair.generate(); // Use your actual keypair
  const recipient = new PublicKey('&#x3C;recipient-public-key>');

<strong>  // Connect to NextBlock (see connection.md)
</strong>  // const config = configFromEnv();
  // const manager = new NextBlockConnectionManager(config);
  // await manager.connect();
  // const nextblockClient = manager.grpcClient;

  try {
<strong>    // Example 1: Basic transaction submission
</strong>    console.log('1. Basic transaction submission:');
    const signature1 = await submitSingleTransaction(
      // nextblockClient,
      connection,
      signer,
      recipient,
      10_000,     // Transfer 10,000 lamports
      1_000_000   // Tip 1,000,000 lamports (0.001 SOL)
    );
    console.log(`Basic transaction: ${signature1}\n`);

<strong>    // Example 2: Transaction with optimal tip
</strong>    console.log('2. Transaction with optimal tip:');
    const signature2 = await submitWithOptimalTip(
      // nextblockClient,
      connection,
      signer,
      recipient,
      20_000,      // Transfer 20,000 lamports
      'aggressive' // Use aggressive tip strategy
    );
    console.log(`Optimally tipped transaction: ${signature2}\n`);

<strong>    // Example 3: Custom transaction builder
</strong>    console.log('3. Custom transaction builder:');
    const builder = new TransactionBuilder(signer);
    const signature3 = await builder
      .addInstruction(SystemProgram.transfer({
        fromPubkey: signer.publicKey,
        toPubkey: recipient,
        lamports: 30_000,
      }))
      .setTipAmount(1_500_000)
      .buildAndSubmit(
        // nextblockClient,
        connection
      );
    console.log(`Custom built transaction: ${signature3}\n`);

<strong>    // Example 4: Multiple concurrent transactions
</strong>    console.log('4. Multiple concurrent transactions:');
    const transactionsData = [
      { recipient, transferAmount: 5_000, tipAmount: 500_000 },
      { recipient, transferAmount: 7_500, tipAmount: 750_000 },
      { recipient, transferAmount: 12_500, tipAmount: 1_250_000 },
    ];

    const signatures = await submitMultipleSingleTransactions(
      // nextblockClient,
      connection,
      signer,
      transactionsData
    );
    console.log(`Multiple transactions: ${signatures}\n`);

<strong>    // Example 5: Transaction monitoring
</strong>    console.log('5. Transaction with performance monitoring:');
    const metrics = new TransactionMetrics();

    for (let i = 0; i &#x3C; 5; i++) {
      await submitWithMonitoring(
        // nextblockClient,
        connection,
        signer,
        recipient,
        1_000 * (i + 1), // Varying amounts
        500_000,         // Fixed tip
        metrics
      );
    }

    console.log('Performance metrics:', metrics.getStats());

  } catch (error) {
    console.error('Error in main:', error);
  } finally {
<strong>    // Clean up connection
</strong>    // await manager.disconnect();
  }
}

<strong>// Error handling with retry logic
</strong>async function submitWithRetry(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair,
  recipient: PublicKey,
  transferAmount: number,
  tipAmount: number,
  maxRetries: number = 3
): Promise&#x3C;string> {
  let lastError: Error;

  for (let attempt = 1; attempt &#x3C;= maxRetries; attempt++) {
    try {
      return await submitSingleTransaction(
        // nextblockClient,
        connection,
        signer,
        recipient,
        transferAmount,
        tipAmount
      );
    } catch (error) {
      lastError = error as Error;
      
      if (attempt &#x3C; maxRetries) {
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000);
        console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError!;
}

if (require.main === module) {
  main().catch(console.error);
}

export {
  submitSingleTransaction,
  submitWithOptimalTip,
  TransactionBuilder,
  submitMultipleSingleTransactions,
  TransactionMetrics,
  submitWithMonitoring,
  submitWithRetry,
};
</code></pre>

## Best Practices

1. **Always include tips**: NextBlock prioritizes transactions with appropriate tips
2. **Use random tip wallets**: Distribute load across multiple tip addresses
3. **Monitor tip floors**: Adjust tips based on current network conditions
4. **Handle errors gracefully**: Implement retry logic with exponential backoff
5. **Validate inputs**: Always validate public keys and amounts before submission
6. **Use TypeScript**: Leverage type safety for better development experience
7. **Monitor performance**: Track success rates and response times
8. **Choose appropriate RPC endpoints**: Use reliable RPC providers for blockhash retrieval


# Submit Batched Transactions

Submit 2-4 transactions as an atomic bundle to NextBlock using JavaScript/TypeScript.

This example shows bundle construction and the request shape you send to NextBlock. Replace the placeholder generated client types with the client generated from `nextblock-proto`.

<pre class="language-typescript"><code class="lang-typescript"><strong>import { 
</strong>  Connection, 
  Keypair, 
  PublicKey, 
  SystemProgram, 
  Transaction 
} from '@solana/web3.js';

<strong>// Transaction bundle builder
</strong>class TransactionBundle {
  private transactions: Transaction[] = [];
  private readonly maxSize: number = 4;
  private readonly minSize: number = 2;

  addTransaction(transaction: Transaction): TransactionBundle {
    if (this.transactions.length >= this.maxSize) {
      throw new Error(`Bundle cannot contain more than ${this.maxSize} transactions`);
    }
    
    this.transactions.push(transaction);
    return this;
  }

  validate(): boolean {
    if (this.transactions.length &#x3C; this.minSize) {
      throw new Error(`Bundle must contain at least ${this.minSize} transactions`);
    }

    if (this.transactions.length > this.maxSize) {
      throw new Error(`Bundle cannot contain more than ${this.maxSize} transactions`);
    }

<strong>    // Check for duplicate signatures
</strong>    const signatures = new Set();
    for (const tx of this.transactions) {
      const sig = tx.signature?.toString();
      if (sig &#x26;&#x26; signatures.has(sig)) {
        throw new Error('Duplicate transaction signatures in bundle');
      }
      if (sig) signatures.add(sig);
    }

    return true;
  }

  toBase64Transactions(): string[] {
    this.validate();
    
    return this.transactions.map(tx => {
      const serialized = tx.serialize();
      return serialized.toString('base64');
    });
  }

  getSignatures(): string[] {
    return this.transactions
      .map(tx => tx.signature?.toString())
      .filter((sig): sig is string => sig !== undefined);
  }

  get size(): number {
    return this.transactions.length;
  }
}

<strong>// Build single transaction with tip
</strong>async function buildTransactionWithTip(
  connection: Connection,
  payer: Keypair,
  tipAmount: number,
  instructions: any[]
): Promise&#x3C;Transaction> {
  const { blockhash } = await connection.getLatestBlockhash('finalized');
  
<strong>  // Random tip wallet for load balancing
</strong>  const tipWallets = [
    'NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid',
    'nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc',
    'NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE',
    'NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2',
  ];
  
  const tipWallet = new PublicKey(tipWallets[Math.floor(Math.random() * tipWallets.length)]);
  
<strong>  // Create tip instruction (should be first)
</strong>  const tipInstruction = SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: tipWallet,
    lamports: tipAmount,
  });

<strong>  // Create and sign transaction
</strong>  const transaction = new Transaction({
    recentBlockhash: blockhash,
    feePayer: payer.publicKey,
  });

  transaction.add(tipInstruction);
  instructions.forEach(instruction => transaction.add(instruction));
  transaction.sign(payer);

  return transaction;
}

<strong>// Submit batched transactions
</strong>async function submitBatchedTransactions(
  // nextblockClient: any, // Your generated gRPC client
  connection: Connection,
  signer: Keypair,
  transactionSpecs: Array&#x3C;{
    instructions: any[];
    tipAmount: number;
  }>
): Promise&#x3C;string> {
<strong>  // Build transaction bundle
</strong>  const bundle = new TransactionBundle();
  
  for (const { instructions, tipAmount } of transactionSpecs) {
    const transaction = await buildTransactionWithTip(
      connection,
      signer,
      tipAmount,
      instructions
    );
    bundle.addTransaction(transaction);
  }

<strong>  // Get base64 transactions for submission
</strong>  const base64Transactions = bundle.toBase64Transactions();
  const signatures = bundle.getSignatures();

<strong>  // Log transaction signatures
</strong>  signatures.forEach((sig, i) => {
    console.log(`Transaction ${i + 1} signature: ${sig}`);
  });

<strong>  // Submit bundle to NextBlock
</strong>  /* Uncomment when you have the generated gRPC client
  const entries = base64Transactions.map(base64Tx => ({
    transaction: { content: base64Tx }
  }));

  const request = { entries };
  const response = await nextblockClient.postSubmitBatchV2(request);

  console.log('Batch submitted successfully!');
  console.log(`Bundle signature: ${response.signature}`);

  return response.signature;
  */

<strong>  // Until your generated client is wired in, return a placeholder bundle signature
</strong>  const bundleSignature = '';
  console.log(`Local bundle prepared with ${bundle.size} transactions.`);
  return bundleSignature;
}

<strong>// Build arbitrage bundle
</strong>async function buildArbitrageBundle(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair,
  dexAAddress: PublicKey,
  dexBAddress: PublicKey,
  tradeAmount: number
): Promise&#x3C;string> {
<strong>  // Transaction 1: Buy on DEX A
</strong>  const buyInstructions = [
<strong>    // Add your DEX-specific buy instructions here
</strong>    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: dexAAddress,
      lamports: tradeAmount,
    })
  ];

<strong>  // Transaction 2: Sell on DEX B
</strong>  const sellInstructions = [
<strong>    // Add your DEX-specific sell instructions here
</strong>    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: dexBAddress,
      lamports: tradeAmount,
    })
  ];

  const transactionSpecs = [
    { instructions: buyInstructions, tipAmount: 2_000_000 },   // Higher tip for arbitrage
    { instructions: sellInstructions, tipAmount: 2_000_000 },  // Higher tip for arbitrage
  ];

  return await submitBatchedTransactions(
    // nextblockClient,
    connection,
    signer,
    transactionSpecs
  );
}

<strong>// Build complex DeFi operation bundle
</strong>async function buildDeFiOperationBundle(
  // nextblockClient: any,
  connection: Connection,
  signer: Keypair
): Promise&#x3C;string> {
<strong>  // Transaction 1: Setup - Create token accounts
</strong>  const setupInstructions = [
<strong>    // Add token account creation instructions
</strong>    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: new PublicKey('&#x3C;token-program-address>'),
      lamports: 1_000_000 // Rent exemption
    })
  ];

<strong>  // Transaction 2: Main operation - Execute swap
</strong>  const swapInstructions = [
<strong>    // Add swap instructions (Jupiter, Raydium, etc.)
</strong>    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: new PublicKey('&#x3C;swap-program-address>'),
      lamports: 0 // No SOL transfer for swap
    })
  ];

<strong>  // Transaction 3: Cleanup - Stake or provide liquidity
</strong>  const stakeInstructions = [
<strong>    // Add staking/liquidity instructions
</strong>    SystemProgram.transfer({
      fromPubkey: signer.publicKey,
      toPubkey: new PublicKey('&#x3C;stake-pool-address>'),
      lamports: 0
    })
  ];

  const transactionSpecs = [
    { instructions: setupInstructions, tipAmount: 500_000 },    // Setup tip
    { instructions: swapInstructions, tipAmount: 1_500_000 },   // Main operation tip
    { instructions: stakeInstructions, tipAmount: 750_000 },    // Cleanup tip
  ];

  return await submitBatchedTransactions(
    // nextblockClient,
    connection,
    signer,
    transactionSpecs
  );
}

<strong>// Bundle optimizer
</strong>class BundleOptimizer {
  private tipMultipliers = {
    setup: 0.5,      // Lower priority
    main: 1.5,       // Higher priority
    cleanup: 0.75,   // Medium priority
    arbitrage: 2.0,  // Highest priority
  };

  optimizeTips(baseTip: number, transactionTypes: string[]): number[] {
    return transactionTypes.map(txType => {
      const multiplier = this.tipMultipliers[txType as keyof typeof this.tipMultipliers] || 1.0;
      return Math.floor(baseTip * multiplier);
    });
  }

  reorderTransactions&#x3C;T>(
    transactions: T[], 
    transactionTypes: string[]
  ): { transactions: T[]; types: string[] } {
<strong>    // Priority order: setup -> main -> cleanup
</strong>    const priorityOrder = { setup: 1, main: 2, cleanup: 3, arbitrage: 0 };
    
    const pairs = transactions.map((tx, i) => ({
      transaction: tx,
      type: transactionTypes[i],
      priority: priorityOrder[transactionTypes[i] as keyof typeof priorityOrder] || 2
    }));

    pairs.sort((a, b) => a.priority - b.priority);

    return {
      transactions: pairs.map(p => p.transaction),
      types: pairs.map(p => p.type)
    };
  }
}

<strong>// Bundle status tracking
</strong>interface BundleStatus {
  bundleId: string;
  transactionCount: number;
  submittedAt: number;
  signatures: string[];
  status: 'pending' | 'confirmed' | 'failed';
}

class BundleTracker {
  private bundles: Map&#x3C;string, BundleStatus> = new Map();

  trackBundle(bundleStatus: BundleStatus): void {
    this.bundles.set(bundleStatus.bundleId, bundleStatus);
  }

  async checkBundleStatus(
    bundleId: string,
    connection: Connection
  ): Promise&#x3C;BundleStatus | null> {
    const bundleStatus = this.bundles.get(bundleId);
    if (!bundleStatus) return null;

<strong>    // Check if all transactions are confirmed
</strong>    let confirmedCount = 0;
    for (const signature of bundleStatus.signatures) {
      try {
        const status = await connection.getSignatureStatus(signature);
        if (status.value?.confirmationStatus) {
          confirmedCount++;
        }
      } catch (error) {
        console.error(`Failed to check signature ${signature}:`, error);
      }
    }

<strong>    // Update bundle status
</strong>    if (confirmedCount === bundleStatus.transactionCount) {
      bundleStatus.status = 'confirmed';
    } else if (Date.now() - bundleStatus.submittedAt > 60000) { // Timeout after 60 seconds
      bundleStatus.status = 'failed';
    }

    return bundleStatus;
  }

  getBundleStatus(bundleId: string): BundleStatus | null {
    return this.bundles.get(bundleId) || null;
  }
}
</code></pre>

## Usage Examples

<pre class="language-typescript"><code class="lang-typescript"><strong>async function main() {
</strong><strong>  // Initialize Solana connection
</strong>  const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
  const signer = Keypair.generate(); // Use your actual keypair

<strong>  // Connect to NextBlock (see connection.md)
</strong>  // const config = configFromEnv();
  // const manager = new NextBlockConnectionManager(config);
  // await manager.connect();
  // const nextblockClient = manager.grpcClient;

  try {
<strong>    // Example 1: Basic batch submission
</strong>    console.log('1. Basic batch submission:');
    const transactionSpecs = [
<strong>      // Setup transaction
</strong>      {
        instructions: [SystemProgram.transfer({
          fromPubkey: signer.publicKey,
          toPubkey: new PublicKey('&#x3C;recipient1>'),
          lamports: 100_000,
        })],
        tipAmount: 500_000, // 0.0005 SOL tip
      },
<strong>      // Main transaction
</strong>      {
        instructions: [SystemProgram.transfer({
          fromPubkey: signer.publicKey,
          toPubkey: new PublicKey('&#x3C;recipient2>'),
          lamports: 200_000,
        })],
        tipAmount: 1_000_000, // 0.001 SOL tip
      },
<strong>      // Cleanup transaction
</strong>      {
        instructions: [SystemProgram.transfer({
          fromPubkey: signer.publicKey,
          toPubkey: new PublicKey('&#x3C;recipient3>'),
          lamports: 50_000,
        })],
        tipAmount: 500_000, // 0.0005 SOL tip
      },
    ];

    const bundleSignature = await submitBatchedTransactions(
      // nextblockClient,
      connection,
      signer,
      transactionSpecs
    );
    console.log(`Basic batch: ${bundleSignature}\n`);

<strong>    // Example 2: Arbitrage bundle
</strong>    console.log('2. Arbitrage bundle:');
    const arbSignature = await buildArbitrageBundle(
      // nextblockClient,
      connection,
      signer,
      new PublicKey('&#x3C;dex-a-address>'),
      new PublicKey('&#x3C;dex-b-address>'),
      1_000_000 // 0.001 SOL trade
    );
    console.log(`Arbitrage bundle: ${arbSignature}\n`);

<strong>    // Example 3: Optimized DeFi bundle
</strong>    console.log('3. DeFi operation bundle:');
    const defiSignature = await buildDeFiOperationBundle(
      // nextblockClient,
      connection,
      signer
    );
    console.log(`DeFi bundle: ${defiSignature}\n`);

<strong>    // Example 4: Bundle optimization
</strong>    console.log('4. Bundle optimization:');
    const optimizer = new BundleOptimizer();
    const baseTip = 1_000_000;
    const transactionTypes = ['setup', 'main', 'cleanup'];
    const optimizedTips = optimizer.optimizeTips(baseTip, transactionTypes);

    console.log(`Optimized tips: ${optimizedTips}`);

<strong>    // Example 5: Bundle tracking
</strong>    console.log('5. Bundle tracking:');
    const tracker = new BundleTracker();
    
    const bundleStatus: BundleStatus = {
      bundleId: 'bundle-123',
      transactionCount: 3,
      submittedAt: Date.now(),
      signatures: ['sig1', 'sig2', 'sig3'],
      status: 'pending'
    };
    
    tracker.trackBundle(bundleStatus);
    
<strong>    // Check status after some time
</strong>    setTimeout(async () => {
      const status = await tracker.checkBundleStatus('bundle-123', connection);
      console.log('Bundle status:', status);
    }, 5000);

  } catch (error) {
    console.error('Error in main:', error);
  } finally {
<strong>    // Clean up connection
</strong>    // await manager.disconnect();
  }
}

if (require.main === module) {
  main().catch(console.error);
}

export {
  TransactionBundle,
  submitBatchedTransactions,
  buildArbitrageBundle,
  buildDeFiOperationBundle,
  BundleOptimizer,
  BundleTracker,
};
</code></pre>

## Best Practices

1. **Bundle size limits**: Keep bundles between 2-4 transactions for optimal success rates
2. **Transaction ordering**: Setup → Main operations → Cleanup
3. **Progressive tipping**: Use higher tips for more critical transactions
4. **Error handling**: Validate bundles before submission
5. **Performance monitoring**: Track bundle success rates and timing
6. **Tip optimization**: Adjust tips based on transaction importance and network conditions
7. **Use TypeScript**: Leverage type safety for complex bundle operations


# Tip Floor Stream

Stream real-time tip floor data from NextBlock using JavaScript/TypeScript.

<pre class="language-typescript"><code class="lang-typescript"><strong>// Tip floor data interface
</strong>interface TipFloorData {
  time: string;
  landed_tips_25th_percentile: number;
  landed_tips_50th_percentile: number;
  landed_tips_75th_percentile: number;
  landed_tips_95th_percentile: number;
  landed_tips_99th_percentile: number;
  ema_landed_tips_50th_percentile: number;
}

<strong>// Tip strategy management
</strong>class TipStrategy {
  conservativeTip: number = 0;     // Will be set from tip floor data
  normalTip: number = 0;          // Will be set from tip floor data
  aggressiveTip: number = 0;      // Will be set from tip floor data
  priorityTip: number = 0;        // Will be set from tip floor data
  lastUpdated?: Date;

  updateFromTipFloor(tipFloor: TipFloorData): void {
    this.conservativeTip = this.solToLamports(tipFloor.landed_tips_25th_percentile);
    this.normalTip = this.solToLamports(tipFloor.landed_tips_50th_percentile);
    this.aggressiveTip = this.solToLamports(tipFloor.landed_tips_75th_percentile);
    this.priorityTip = this.solToLamports(tipFloor.landed_tips_95th_percentile);
    this.lastUpdated = new Date();
  }

  private solToLamports(sol: number): number {
    return Math.floor(sol * 1_000_000_000);
  }

  getTipForPriority(priority: 'conservative' | 'normal' | 'aggressive' | 'priority'): number {
    switch (priority) {
      case 'conservative': return this.conservativeTip;
      case 'normal': return this.normalTip;
      case 'aggressive': return this.aggressiveTip;
      case 'priority': return this.priorityTip;
      default: return this.normalTip;
    }
  }
}

<strong>// Global tip strategy instance
</strong>const globalTipStrategy = new TipStrategy();

<strong>// Stream tip floor data
</strong>async function streamTipFloor(
  // nextblockClient: any, // Your generated gRPC client
  updateFrequency: string = '1m',
  callback?: (tipFloor: TipFloorData) => Promise&#x3C;void> | void
): Promise&#x3C;void> {
  console.log(`Starting tip floor stream with frequency: ${updateFrequency}`);

  /* Uncomment when you have the generated gRPC client
  try {
    const request = { updateFrequency };
    const stream = nextblockClient.streamTipFloor(request);

    console.log('Streaming tip floor data:');

    for await (const tipFloorResponse of stream) {
      try {
        const tipFloor: TipFloorData = {
          time: tipFloorResponse.time,
          landed_tips_25th_percentile: tipFloorResponse.landed_tips_25th_percentile,
          landed_tips_50th_percentile: tipFloorResponse.landed_tips_50th_percentile,
          landed_tips_75th_percentile: tipFloorResponse.landed_tips_75th_percentile,
          landed_tips_95th_percentile: tipFloorResponse.landed_tips_95th_percentile,
          landed_tips_99th_percentile: tipFloorResponse.landed_tips_99th_percentile,
          ema_landed_tips_50th_percentile: tipFloorResponse.ema_landed_tips_50th_percentile,
        };

        console.log('Received tip floor update:');
        console.log(`  Time: ${tipFloor.time}`);
        console.log(`  25th percentile: ${tipFloor.landed_tips_25th_percentile.toFixed(6)} SOL`);
        console.log(`  50th percentile: ${tipFloor.landed_tips_50th_percentile.toFixed(6)} SOL`);
        console.log(`  75th percentile: ${tipFloor.landed_tips_75th_percentile.toFixed(6)} SOL`);
        console.log(`  95th percentile: ${tipFloor.landed_tips_95th_percentile.toFixed(6)} SOL`);
        console.log(`  EMA 50th percentile: ${tipFloor.ema_landed_tips_50th_percentile.toFixed(6)} SOL`);
        console.log('  ---');

        // Update global tip strategy
        await processTipFloorUpdate(tipFloor);

        // Call custom callback if provided
        if (callback) {
          await callback(tipFloor);
        }

      } catch (error) {
        console.error('Error processing tip floor update:', error);
      }
    }

  } catch (error) {
    console.error('Tip floor stream error:', error);
    // Implement reconnection logic here
  }
  */

<strong>  // Mock streaming for demonstration
</strong>  console.log('Mock tip floor streaming started...');

  const mockStream = setInterval(async () => {
<strong>    // Generate mock tip floor data
</strong>    const mockTipFloor: TipFloorData = {
      time: new Date().toISOString(),
      landed_tips_25th_percentile: 0.0011,
      landed_tips_50th_percentile: 0.005000001,
      landed_tips_75th_percentile: 0.01555,
      landed_tips_95th_percentile: 0.09339195639999975,
      landed_tips_99th_percentile: 0.4846427910400001,
      ema_landed_tips_50th_percentile: 0.005989477267191758,
    };

    console.log('Mock tip floor update:', mockTipFloor);
    await processTipFloorUpdate(mockTipFloor);

    if (callback) {
      await callback(mockTipFloor);
    }
  }, 60000); // Update every minute

<strong>  // Return a promise that never resolves (keeps streaming)
</strong>  return new Promise(() => {
    // Keep the interval running
    process.on('SIGINT', () => {
      clearInterval(mockStream);
      console.log('Tip floor streaming stopped');
      process.exit(0);
    });
  });
}

<strong>// Process tip floor updates
</strong>async function processTipFloorUpdate(tipFloor: TipFloorData): Promise&#x3C;void> {
<strong>  // Update global strategy
</strong>  globalTipStrategy.updateFromTipFloor(tipFloor);

  console.log('Updated tip strategy:');
  console.log(`  Conservative: ${globalTipStrategy.conservativeTip} lamports`);
  console.log(`  Normal: ${globalTipStrategy.normalTip} lamports`);
  console.log(`  Aggressive: ${globalTipStrategy.aggressiveTip} lamports`);
  console.log(`  Priority: ${globalTipStrategy.priorityTip} lamports`);

<strong>  // Store historical data
</strong>  await storeTipFloorData(tipFloor);

<strong>  // Trigger any pending transactions
</strong>  await triggerPendingTransactions();
}

<strong>// Historical data management
</strong>class TipFloorHistory {
  private data: TipFloorData[] = [];
  private readonly maxSize: number;

  constructor(maxSize: number = 1000) {
    this.maxSize = maxSize;
  }

  add(tipFloor: TipFloorData): void {
    if (this.data.length >= this.maxSize) {
      this.data.shift(); // Remove oldest
    }
    this.data.push(tipFloor);
  }

  getTrend(percentile: '25th' | '50th' | '75th' | '95th' = '50th', window: number = 10): number {
    if (this.data.length &#x3C; 2) return 0;

    const recentData = this.data.slice(-window);
    if (recentData.length &#x3C; 2) return 0;

    const key = `landed_tips_${percentile}_percentile` as keyof TipFloorData;
    const startValue = recentData[0][key] as number;
    const endValue = recentData[recentData.length - 1][key] as number;

    return endValue - startValue;
  }

  getAverage(percentile: '25th' | '50th' | '75th' | '95th' = '50th', window: number = 10): number {
    if (this.data.length === 0) return 0;

    const recentData = this.data.slice(-window);
    const key = `landed_tips_${percentile}_percentile` as keyof TipFloorData;
    const values = recentData.map(d => d[key] as number);

    return values.reduce((sum, val) => sum + val, 0) / values.length;
  }

  get length(): number {
    return this.data.length;
  }
}

<strong>// Global history tracker
</strong>const tipFloorHistory = new TipFloorHistory();

<strong>// Smart tip calculation with trend analysis
</strong>async function getSmartTip(
  basePriority: 'conservative' | 'normal' | 'aggressive' | 'priority' = 'normal',
  considerTrend: boolean = true
): Promise&#x3C;number> {
  const baseTip = globalTipStrategy.getTipForPriority(basePriority);

  if (!considerTrend || tipFloorHistory.length &#x3C; 2) {
    return baseTip;
  }

<strong>  // Analyze trend
</strong>  const trend = tipFloorHistory.getTrend('50th', 5);

<strong>  // Adjust tip based on trend
</strong>  let adjustmentFactor = 1.0;
  if (trend > 0.001) {
    adjustmentFactor = 1.2;
    console.log(`Tips trending up (+${trend.toFixed(6)}), increasing tip by 20%`);
  } else if (trend &#x3C; -0.001) {
    adjustmentFactor = 0.9;
    console.log(`Tips trending down (${trend.toFixed(6)}), decreasing tip by 10%`);
  } else {
    console.log(`Tips stable (${trend.toFixed(6)}), no adjustment`);
  }

  const smartTip = Math.floor(baseTip * adjustmentFactor);
  return Math.max(smartTip, 100_000); // Minimum tip of 0.0001 SOL
}

<strong>// Store tip floor data
</strong>async function storeTipFloorData(tipFloor: TipFloorData): Promise&#x3C;void> {
<strong>  // Add to history
</strong>  tipFloorHistory.add(tipFloor);

<strong>  // Optionally save to file
</strong>  const fs = require('fs').promises;
  const filename = `tip_data_${new Date().toISOString().split('T')[0]}.jsonl`;

  try {
    await fs.appendFile(filename, JSON.stringify(tipFloor) + '\n');
  } catch (error) {
    console.error('Failed to store tip floor data:', error);
  }
}

<strong>// Trigger pending transactions
</strong>async function triggerPendingTransactions(): Promise&#x3C;void> {
  console.log('Checking for pending transactions to trigger...');
<strong>  // Implementation would check your pending transaction queue
</strong>  // and submit them with updated tip amounts
}

<strong>// Get current optimal tips
</strong>function getCurrentOptimalTips(): {
  conservative: number;
  normal: number;
  aggressive: number;
  priority: number;
} {
  return {
    conservative: globalTipStrategy.getTipForPriority('conservative'),
    normal: globalTipStrategy.getTipForPriority('normal'),
    aggressive: globalTipStrategy.getTipForPriority('aggressive'),
    priority: globalTipStrategy.getTipForPriority('priority'),
  };
}
</code></pre>

## Usage Examples

<pre class="language-typescript"><code class="lang-typescript"><strong>async function tipFloorExample() {
</strong><strong>  // Connect to NextBlock (see connection.md)
</strong>  // const config = configFromEnv();
  // const manager = new NextBlockConnectionManager(config);
  // await manager.connect();
  // const nextblockClient = manager.grpcClient;

<strong>  // Custom callback for tip floor updates
</strong>  const onTipFloorUpdate = async (tipFloor: TipFloorData) => {
    console.log(`Custom handler: Received update at ${tipFloor.time}`);

<strong>    // Example: Trigger high-priority transactions when tips are low
</strong>    if (tipFloor.landed_tips_50th_percentile &#x3C; 0.002) { // Less than 0.002 SOL
      console.log('Tips are low - good time for high-priority transactions!');
      // await submitPriorityTransactions();
    }
  };

<strong>  // Start streaming in background
</strong>  const streamPromise = streamTipFloor(
    // nextblockClient,
    '1m',
    onTipFloorUpdate
  );

<strong>  // Example usage of dynamic tips
</strong>  setTimeout(async () => {
<strong>    // Get current optimal tips
</strong>    const currentTips = getCurrentOptimalTips();
    console.log('Current optimal tips:', currentTips);

<strong>    // Get smart tip with trend analysis
</strong>    const smartTip = await getSmartTip('normal', true);
    console.log(`Smart tip: ${smartTip} lamports`);

<strong>    // Example: Use tips in transaction submission
</strong>    // await submitTransactionWithTip(smartTip);

  }, 5000); // Wait for initial data

<strong>  // Keep streaming
</strong>  try {
    await streamPromise;
  } catch (error) {
    console.error('Streaming error:', error);
  }
}

<strong>// Advanced tip management example
</strong>async function advancedTipManagement() {
<strong>  // Monitor tip trends and adjust strategy
</strong>  const monitorTrends = setInterval(async () => {
    if (tipFloorHistory.length >= 10) {
      const trend5min = tipFloorHistory.getTrend('50th', 5);
      const trend10min = tipFloorHistory.getTrend('50th', 10);
      const average = tipFloorHistory.getAverage('50th', 10);

      console.log('Tip Analysis:');
      console.log(`  5-min trend: ${trend5min.toFixed(6)} SOL`);
      console.log(`  10-min trend: ${trend10min.toFixed(6)} SOL`);
      console.log(`  10-min average: ${average.toFixed(6)} SOL`);

<strong>      // Adjust strategy based on trends
</strong>      if (trend5min > 0.002 &#x26;&#x26; trend10min > 0.001) {
        console.log('Strong upward trend detected - consider higher tips');
      } else if (trend5min &#x3C; -0.002 &#x26;&#x26; trend10min &#x3C; -0.001) {
        console.log('Strong downward trend detected - can use lower tips');
      }
    }
  }, 300000); // Check every 5 minutes

<strong>  // Clean up on exit
</strong>  process.on('SIGINT', () => {
    clearInterval(monitorTrends);
    console.log('Advanced tip management stopped');
    process.exit(0);
  });
}

<strong>// Main example runner
</strong>async function main() {
  console.log('NextBlock Tip Floor Streaming Examples');

  try {
<strong>    // Start tip floor streaming
</strong>    console.log('Starting tip floor streaming...');
    
<strong>    // Run both examples concurrently
</strong>    await Promise.all([
      tipFloorExample(),
      advancedTipManagement()
    ]);

  } catch (error) {
    console.error('Error in main:', error);
  }
}

if (require.main === module) {
  main().catch(console.error);
}

export {
  TipFloorData,
  TipStrategy,
  TipFloorHistory,
  streamTipFloor,
  getSmartTip,
  getCurrentOptimalTips,
  globalTipStrategy,
};
</code></pre>


# Keepalive

Maintain persistent gRPC connections to NextBlock using JavaScript/TypeScript.

<pre class="language-typescript"><code class="lang-typescript"><strong>// Connection health tracking
</strong>interface ConnectionHealth {
  isHealthy: boolean;
  lastSuccessfulPing: Date;
  consecutiveFailures: number;
  totalPingsSent: number;
  totalPingsSuccessful: number;
  averagePingTime: number;
}

<strong>// Keepalive configuration
</strong>interface KeepaliveConfig {
  pingInterval: number;        // milliseconds
  maxConsecutiveFailures: number;
  reconnectDelay: number;      // milliseconds
  healthCheckEnabled: boolean;
  timeout: number;             // milliseconds
}

<strong>// Default keepalive configuration
</strong>const defaultKeepaliveConfig: KeepaliveConfig = {
  pingInterval: 60000,         // 60 seconds
  maxConsecutiveFailures: 3,
  reconnectDelay: 5000,        // 5 seconds
  healthCheckEnabled: true,
  timeout: 15000,              // 15 seconds
};

<strong>// Basic keepalive implementation
</strong>async function startKeepaliveTask(
  // nextblockClient: any, // Your generated gRPC client
  config: KeepaliveConfig = defaultKeepaliveConfig
): Promise&#x3C;void> {
  console.log(`Starting keepalive task with ${config.pingInterval}ms interval`);

  const keepaliveInterval = setInterval(async () => {
    try {
      const startTime = Date.now();

      /* Uncomment when you have the generated gRPC client
      try {
        await Promise.race([
          nextblockClient.ping({}),
          new Promise((_, reject) => 
            setTimeout(() => reject(new Error('Timeout')), config.timeout)
          )
        ]);

        const pingTime = Date.now() - startTime;
        console.log(`Keepalive ping successful (${pingTime}ms) at ${new Date().toTimeString()}`);

      } catch (error) {
        console.error('Keepalive ping failed:', error);
        // Optionally implement reconnection logic
        clearInterval(keepaliveInterval);
        return;
      }
      */

<strong>      // Mock ping for demonstration
</strong>      await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 100)); // 50-150ms delay
      const pingTime = Date.now() - startTime;
      console.log(`Mock keepalive ping successful (${pingTime}ms) at ${new Date().toTimeString()}`);

    } catch (error) {
      console.error('Keepalive task error:', error);
      clearInterval(keepaliveInterval);
    }
  }, config.pingInterval);

<strong>  // Handle graceful shutdown
</strong>  process.on('SIGINT', () => {
    clearInterval(keepaliveInterval);
    console.log('Keepalive task stopped');
    process.exit(0);
  });

<strong>  // Return a promise that never resolves (keeps running)
</strong>  return new Promise(() => {});
}

<strong>// Advanced keepalive manager
</strong>class KeepaliveManager {
  private config: KeepaliveConfig;
  private health: ConnectionHealth;
  private keepaliveInterval?: NodeJS.Timeout;
  private isRunning: boolean = false;

  constructor(
    // private nextblockClient: any, // Your generated gRPC client
    config: KeepaliveConfig = defaultKeepaliveConfig
  ) {
    this.config = config;
    this.health = {
      isHealthy: true,
      lastSuccessfulPing: new Date(),
      consecutiveFailures: 0,
      totalPingsSent: 0,
      totalPingsSuccessful: 0,
      averagePingTime: 0,
    };
  }

  async start(): Promise&#x3C;void> {
    if (this.isRunning) return;

    this.isRunning = true;
    this.keepaliveInterval = setInterval(
      () => this.sendPing(),
      this.config.pingInterval
    );
    
    console.log('Keepalive manager started');
  }

  async stop(): Promise&#x3C;void> {
    this.isRunning = false;
    
    if (this.keepaliveInterval) {
      clearInterval(this.keepaliveInterval);
      this.keepaliveInterval = undefined;
    }
    
    console.log('Keepalive manager stopped');
  }

  private async sendPing(): Promise&#x3C;void> {
    const startTime = Date.now();
    
    try {
      /* Uncomment when you have the generated gRPC client
      await Promise.race([
        this.nextblockClient.ping({}),
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('Timeout')), this.config.timeout)
        )
      ]);
      */

<strong>      // Mock ping delay
</strong>      await new Promise(resolve => 
        setTimeout(resolve, 50 + Math.random() * 100)
      );

      const pingTime = Date.now() - startTime;
      this.updateHealthSuccess(pingTime);

      console.log(
        `Keepalive ping successful (${pingTime}ms) - ` +
        `Health: ${this.getSuccessRate().toFixed(1)}%`
      );

    } catch (error) {
      this.updateHealthFailure();
      console.error(`Keepalive ping failed: ${error}`);

      if (!this.health.isHealthy) {
        await this.handleConnectionRecovery();
      }
    }
  }

  private updateHealthSuccess(pingTime: number): void {
    this.health.isHealthy = true;
    this.health.lastSuccessfulPing = new Date();
    this.health.consecutiveFailures = 0;
    this.health.totalPingsSent++;
    this.health.totalPingsSuccessful++;

<strong>    // Update average ping time
</strong>    if (this.health.totalPingsSuccessful === 1) {
      this.health.averagePingTime = pingTime;
    } else {
      this.health.averagePingTime = (
        (this.health.averagePingTime * (this.health.totalPingsSuccessful - 1) + pingTime) /
        this.health.totalPingsSuccessful
      );
    }
  }

  private updateHealthFailure(): void {
    this.health.consecutiveFailures++;
    this.health.totalPingsSent++;

    if (this.health.consecutiveFailures >= this.config.maxConsecutiveFailures) {
      this.health.isHealthy = false;
      console.warn(
        `Connection marked as unhealthy after ${this.health.consecutiveFailures} consecutive failures`
      );
    }
  }

  private async handleConnectionRecovery(): Promise&#x3C;void> {
    console.warn('Connection unhealthy, attempting recovery...');
    
<strong>    // Wait before attempting recovery
</strong>    await new Promise(resolve => setTimeout(resolve, this.config.reconnectDelay));
    
    try {
<strong>      // Implement connection recovery logic here
</strong>      console.log('Connection recovery attempted');
      
<strong>      // Reset some health metrics on successful recovery
</strong>      // this.health.consecutiveFailures = 0;
      
    } catch (error) {
      console.error('Connection recovery failed:', error);
    }
  }

  getHealth(): ConnectionHealth {
    return { ...this.health };
  }

  isHealthy(): boolean {
    return this.health.isHealthy;
  }

  private getSuccessRate(): number {
    if (this.health.totalPingsSent === 0) return 100;
    return (this.health.totalPingsSuccessful / this.health.totalPingsSent) * 100;
  }
}

<strong>// Connection manager with integrated keepalive
</strong>class ConnectionManagerWithKeepalive {
  private nextblockConfig: any;
  private keepaliveConfig: KeepaliveConfig;
  private keepaliveManager?: KeepaliveManager;
  // private client?: any;
  private isConnected: boolean = false;

  constructor(
    nextblockConfig: any,
    keepaliveConfig: KeepaliveConfig = defaultKeepaliveConfig
  ) {
    this.nextblockConfig = nextblockConfig;
    this.keepaliveConfig = keepaliveConfig;
  }

  async connect(): Promise&#x3C;boolean> {
    try {
<strong>      // Create connection (see connection.md)
</strong>      // const connection = await createNextBlockClient(this.nextblockConfig);
      // this.client = connection.client;

<strong>      // Test connection
</strong>      // await this.client.ping({});

      this.isConnected = true;
      console.log('Successfully connected to NextBlock');

<strong>      // Start keepalive
</strong>      this.keepaliveManager = new KeepaliveManager(
        // this.client,
        this.keepaliveConfig
      );
      await this.keepaliveManager.start();

      return true;

    } catch (error) {
      console.error('Failed to connect:', error);
      this.isConnected = false;
      return false;
    }
  }

  async disconnect(): Promise&#x3C;void> {
    if (this.keepaliveManager) {
      await this.keepaliveManager.stop();
    }

    this.isConnected = false;
    console.log('Disconnected from NextBlock');
  }

  getConnectionHealth(): ConnectionHealth | null {
    return this.keepaliveManager?.getHealth() || null;
  }

  get connected(): boolean {
    return this.isConnected;
  }

  // get grpcClient(): any {
  //   return this.client;
  // }
}

<strong>// Health monitoring and alerting
</strong>class HealthMonitor {
  private keepaliveManager: KeepaliveManager;
  private alertThresholds = {
    successRate: 90.0,        // Alert if success rate &#x3C; 90%
    avgPingTime: 1000,        // Alert if avg ping time > 1s
    consecutiveFailures: 2,   // Alert after 2 consecutive failures
  };
  private monitorInterval?: NodeJS.Timeout;

  constructor(keepaliveManager: KeepaliveManager) {
    this.keepaliveManager = keepaliveManager;
  }

  startMonitoring(checkInterval: number = 30000): void {
    this.monitorInterval = setInterval(() => {
      this.checkHealth();
    }, checkInterval);

    console.log(`Health monitoring started with ${checkInterval}ms interval`);
  }

  stopMonitoring(): void {
    if (this.monitorInterval) {
      clearInterval(this.monitorInterval);
      this.monitorInterval = undefined;
    }
    console.log('Health monitoring stopped');
  }

  private checkHealth(): void {
    const health = this.keepaliveManager.getHealth();

<strong>    // Check success rate
</strong>    const successRate = health.totalPingsSent > 0 
      ? (health.totalPingsSuccessful / health.totalPingsSent) * 100 
      : 100;

    if (successRate &#x3C; this.alertThresholds.successRate) {
      this.triggerAlert('Low success rate', `Success rate: ${successRate.toFixed(1)}%`);
    }

<strong>    // Check average ping time
</strong>    if (health.averagePingTime > this.alertThresholds.avgPingTime) {
      this.triggerAlert('High ping time', `Average ping time: ${health.averagePingTime.toFixed(1)}ms`);
    }

<strong>    // Check consecutive failures
</strong>    if (health.consecutiveFailures >= this.alertThresholds.consecutiveFailures) {
      this.triggerAlert('Connection issues', `Consecutive failures: ${health.consecutiveFailures}`);
    }
  }

  private triggerAlert(alertType: string, details: string): void {
    const timestamp = new Date().toISOString();
    console.warn(`🚨 HEALTH ALERT [${timestamp}] ${alertType}: ${details}`);

<strong>    // Implement additional alerting logic here
</strong>    // - Send notifications
    // - Post to monitoring systems
    // - Trigger recovery procedures
  }
}
</code></pre>

## Usage Examples

<pre class="language-typescript"><code class="lang-typescript"><strong>// Basic keepalive example
</strong>async function basicKeepaliveExample(): Promise&#x3C;void> {
  console.log('Starting basic keepalive example...');

<strong>  // Connect to NextBlock (see connection.md)
</strong>  // const config = configFromEnv();
  // const connection = await createNextBlockClient(config);

<strong>  // Start keepalive task
</strong>  const keepalivePromise = startKeepaliveTask(
    // connection.client,
    {
      pingInterval: 30000,     // Ping every 30 seconds
      maxConsecutiveFailures: 3,
      reconnectDelay: 5000,
      healthCheckEnabled: true,
      timeout: 15000,
    }
  );

  console.log('Application running with keepalive...');

<strong>  // Simulate application work
</strong>  setTimeout(() => {
    console.log('Application work completed, stopping...');
    process.exit(0);
  }, 300000); // Run for 5 minutes

  await keepalivePromise;
}

<strong>// Advanced keepalive with health monitoring
</strong>async function advancedKeepaliveExample(): Promise&#x3C;void> {
  console.log('Starting advanced keepalive example...');

<strong>  // Configuration
</strong>  const keepaliveConfig: KeepaliveConfig = {
    pingInterval: 30000,      // Ping every 30 seconds
    maxConsecutiveFailures: 3,
    reconnectDelay: 10000,    // Wait 10 seconds before reconnection
    healthCheckEnabled: true,
    timeout: 15000,
  };

<strong>  // Use connection manager with integrated keepalive
</strong>  const manager = new ConnectionManagerWithKeepalive(
    {}, // nextblockConfig placeholder
    keepaliveConfig
  );

  try {
    const connected = await manager.connect();

    if (connected) {
      console.log('Connected with keepalive enabled');

<strong>      // Start health monitoring
</strong>      const healthMonitor = new HealthMonitor(manager.keepaliveManager!);
      healthMonitor.startMonitoring(60000); // Check every minute

<strong>      // Simulate application work with periodic health checks
</strong>      for (let i = 0; i &#x3C; 10; i++) {
        await new Promise(resolve => setTimeout(resolve, 30000));

        const health = manager.getConnectionHealth();
        if (health) {
          console.log(`Connection health check ${i + 1}:`);
          console.log(`  Healthy: ${health.isHealthy}`);
          console.log(`  Success rate: ${((health.totalPingsSuccessful / Math.max(health.totalPingsSent, 1)) * 100).toFixed(1)}%`);
          console.log(`  Avg ping time: ${health.averagePingTime.toFixed(1)}ms`);
          console.log(`  Total pings: ${health.totalPingsSent}`);
        }
      }

      healthMonitor.stopMonitoring();

    } else {
      console.error('Failed to establish connection');
    }

  } catch (error) {
    console.error('Advanced keepalive example failed:', error);
  } finally {
    await manager.disconnect();
  }
}

<strong>// Connection pool with keepalive
</strong>class ConnectionPoolWithKeepalive {
  private connections: ConnectionManagerWithKeepalive[] = [];
  private currentIndex: number = 0;
  private poolSize: number;
  private nextblockConfig: any;
  private keepaliveConfig: KeepaliveConfig;

  constructor(
    nextblockConfig: any,
    keepaliveConfig: KeepaliveConfig,
    poolSize: number = 5
  ) {
    this.nextblockConfig = nextblockConfig;
    this.keepaliveConfig = keepaliveConfig;
    this.poolSize = poolSize;
  }

  async initialize(): Promise&#x3C;void> {
    console.log(`Initializing connection pool with ${this.poolSize} connections...`);

    const connectionPromises = Array.from({ length: this.poolSize }, async (_, i) => {
      const manager = new ConnectionManagerWithKeepalive(
        this.nextblockConfig,
        this.keepaliveConfig
      );

      const connected = await manager.connect();
      if (connected) {
        this.connections.push(manager);
        console.log(`Connection ${i + 1} established with keepalive`);
      } else {
        console.warn(`Failed to create connection ${i + 1}`);
      }
    });

    await Promise.all(connectionPromises);
    console.log(`Connection pool initialized with ${this.connections.length} connections`);
  }

  getConnection(): ConnectionManagerWithKeepalive {
    if (this.connections.length === 0) {
      throw new Error('No available connections in pool');
    }

    const connection = this.connections[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.connections.length;
    return connection;
  }

  async closeAll(): Promise&#x3C;void> {
    await Promise.all(this.connections.map(conn => conn.disconnect()));
    this.connections = [];
    console.log('All connections closed');
  }

  getPoolHealth(): ConnectionHealth[] {
    return this.connections
      .map(conn => conn.getConnectionHealth())
      .filter((health): health is ConnectionHealth => health !== null);
  }
}

<strong>// Main example runner
</strong>async function main(): Promise&#x3C;void> {
  console.log('NextBlock Keepalive Examples');

  const choice = process.argv[2] || '1';

  switch (choice) {
    case '1':
      console.log('\n1. Basic Keepalive:');
      await basicKeepaliveExample();
      break;
    
    case '2':
      console.log('\n2. Advanced Keepalive with Monitoring:');
      await advancedKeepaliveExample();
      break;
    
    case '3':
      console.log('\n3. Connection Pool with Keepalive:');
      const pool = new ConnectionPoolWithKeepalive(
        {}, // config placeholder
        defaultKeepaliveConfig,
        3
      );
      
      await pool.initialize();
      
<strong>      // Use the pool for some time
</strong>      setTimeout(async () => {
        const poolHealth = pool.getPoolHealth();
        console.log(`Pool health summary: ${poolHealth.length} healthy connections`);
        await pool.closeAll();
      }, 60000);
      
      break;
    
    default:
      console.log('Usage: node keepalive.js [1|2|3]');
      console.log('  1: Basic keepalive');
      console.log('  2: Advanced keepalive with monitoring');
      console.log('  3: Connection pool with keepalive');
  }
}

if (require.main === module) {
  main().catch(console.error);
}

export {
  KeepaliveConfig,
  ConnectionHealth,
  KeepaliveManager,
  ConnectionManagerWithKeepalive,
  HealthMonitor,
  ConnectionPoolWithKeepalive,
  startKeepaliveTask,
};
</code></pre>

## Best Practices

1. **Appropriate intervals**: Use 30-60 second ping intervals for most applications
2. **Health monitoring**: Track connection health and implement alerting
3. **Graceful recovery**: Handle connection failures with exponential backoff
4. **Resource cleanup**: Always stop keepalive tasks when shutting down
5. **Timeout handling**: Set reasonable timeouts for ping requests
6. **Logging**: Log keepalive events for debugging and monitoring
7. **Integration**: Integrate keepalive with your connection management system
8. **Connection pooling**: Use connection pools for high-throughput applications


