diff --git a/cmd/geth/config.go b/cmd/geth/config.go
index 9dd0f7cdf..f16dced85 100644
--- a/cmd/geth/config.go
+++ b/cmd/geth/config.go
@@ -45,6 +45,7 @@ import (
 	"github.com/ethereum/go-ethereum/metrics"
 	"github.com/ethereum/go-ethereum/node"
 	"github.com/ethereum/go-ethereum/params"
+	"github.com/ethereum/go-ethereum/validation"
 	"github.com/naoina/toml"
 	"github.com/urfave/cli/v2"
 )
@@ -93,11 +94,12 @@ type ethstatsConfig struct {
 }
 
 type gethConfig struct {
-	Eth      ethconfig.Config
-	Node     node.Config
-	Ethstats ethstatsConfig
-	Metrics  metrics.Config
-	Builder  builder.Config
+	Eth        ethconfig.Config
+	Node       node.Config
+	Ethstats   ethstatsConfig
+	Metrics    metrics.Config
+	Builder    builder.Config
+	Validation validation.Config
 }
 
 func loadConfig(file string, cfg *gethConfig) error {
@@ -131,10 +133,11 @@ func defaultNodeConfig() node.Config {
 func loadBaseConfig(ctx *cli.Context) gethConfig {
 	// Load defaults.
 	cfg := gethConfig{
-		Eth:     ethconfig.Defaults,
-		Node:    defaultNodeConfig(),
-		Metrics: metrics.DefaultConfig,
-		Builder: builder.DefaultConfig,
+		Eth:        ethconfig.Defaults,
+		Node:       defaultNodeConfig(),
+		Metrics:    metrics.DefaultConfig,
+		Builder:    builder.DefaultConfig,
+		Validation: validation.DefaultConfig,
 	}
 
 	// Load config file.
@@ -170,6 +173,9 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
 	// Apply builder flags
 	utils.SetBuilderConfig(ctx, &cfg.Builder)
 
+	// Apply validation flags
+	utils.SetValidationConfig(ctx, &cfg.Validation)
+
 	return stack, cfg
 }
 
@@ -218,6 +224,10 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
 		utils.Fatalf("Failed to register the Block Validation API: %v", err)
 	}
 
+	if err := validation.Register(stack, eth, &cfg.Validation); err != nil {
+		utils.Fatalf("Failed to register the Validation API: %v", err)
+	}
+
 	// Configure GraphQL if requested
 	if ctx.IsSet(utils.GraphQLEnabledFlag.Name) {
 		utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index d7d63db78..602a46ea1 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -74,6 +74,7 @@ import (
 	"github.com/ethereum/go-ethereum/triedb"
 	"github.com/ethereum/go-ethereum/triedb/hashdb"
 	"github.com/ethereum/go-ethereum/triedb/pathdb"
+	"github.com/ethereum/go-ethereum/validation"
 	pcsclite "github.com/gballet/go-libpcsclite"
 	gopsutil "github.com/shirou/gopsutil/mem"
 	"github.com/urfave/cli/v2"
@@ -810,6 +811,43 @@ var (
 		Category: flags.BuilderCategory,
 	}
 
+	// Block Validation Settings
+	BlockValidationEnabled = &cli.BoolFlag{
+		Name:     "validation",
+		Usage:    "Enable the block validation service",
+		Value:    validation.DefaultConfig.Enabled,
+		Category: flags.ValidationCategory,
+	}
+
+	BlockValidationListenAddr = &cli.StringFlag{
+		Name:     "validation.listen_addr",
+		Usage:    "Listening address for block validation endpoint",
+		Value:    validation.DefaultConfig.ListenAddr,
+		Category: flags.ValidationCategory,
+	}
+
+	BlockValidationBlacklistSourceFilePath = &cli.StringFlag{
+		Name: "validation.blacklist",
+		Usage: "Path to file containing blacklisted addresses, json-encoded list of strings. " +
+			"Builder will ignore transactions that touch mentioned addresses. This flag is also used for block validation API.\n" +
+			"NOTE: builder.blacklist is deprecated and will be removed in the future in favor of validation.blacklist",
+		Category: flags.ValidationCategory,
+	}
+
+	BlockValidationUseBalanceDiff = &cli.BoolFlag{
+		Name:     "validation.use_balance_diff",
+		Usage:    "Block validation API will use fee recipient balance difference for profit calculation.",
+		Value:    validation.DefaultConfig.ExcludeWithdrawals,
+		Category: flags.ValidationCategory,
+	}
+
+	BlockValidationExcludeWithdrawals = &cli.BoolFlag{
+		Name:     "validation.exclude_withdrawals",
+		Usage:    "Block validation API will exclude CL withdrawals to the fee recipient from the balance delta.",
+		Value:    validation.DefaultConfig.ExcludeWithdrawals,
+		Category: flags.ValidationCategory,
+	}
+
 	// RPC settings
 	IPCDisabledFlag = &cli.BoolFlag{
 		Name:     "ipcdisable",
@@ -1641,6 +1679,37 @@ func SetBuilderConfig(ctx *cli.Context, cfg *builder.Config) {
 	cfg.BlockProcessorURL = ctx.String(BuilderBlockProcessorURL.Name)
 }
 
+// SetValidationConfig applies node-related command line flags to the block validation config.
+func SetValidationConfig(ctx *cli.Context, cfg *validation.Config) {
+	if ctx.IsSet(BlockValidationEnabled.Name) {
+		cfg.Enabled = ctx.Bool(BlockValidationEnabled.Name)
+	}
+
+	// this flag should be deprecated in favor of the validation api
+	if ctx.IsSet(BuilderBlockValidationBlacklistSourceFilePath.Name) {
+		cfg.Blocklist = ctx.String(BuilderBlockValidationBlacklistSourceFilePath.Name)
+	}
+	if ctx.IsSet(BlockValidationBlacklistSourceFilePath.Name) {
+		cfg.Blocklist = ctx.String(BlockValidationBlacklistSourceFilePath.Name)
+	}
+
+	// this flag should be deprecated in favor of the validation api
+	if ctx.IsSet(BuilderBlockValidationUseBalanceDiff.Name) {
+		cfg.UseCoinbaseDiff = ctx.Bool(BuilderBlockValidationUseBalanceDiff.Name)
+	}
+	if ctx.IsSet(BlockValidationUseBalanceDiff.Name) {
+		cfg.UseCoinbaseDiff = ctx.Bool(BlockValidationUseBalanceDiff.Name)
+	}
+
+	// this flag should be deprecated in favor of the validation api
+	if ctx.IsSet(BuilderBlockValidationExcludeWithdrawals.Name) {
+		cfg.ExcludeWithdrawals = ctx.Bool(BuilderBlockValidationExcludeWithdrawals.Name)
+	}
+	if ctx.IsSet(BlockValidationExcludeWithdrawals.Name) {
+		cfg.ExcludeWithdrawals = ctx.Bool(BlockValidationExcludeWithdrawals.Name)
+	}
+}
+
 // SetNodeConfig applies node-related command line flags to the config.
 func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
 	SetP2PConfig(ctx, &cfg.P2P)
diff --git a/internal/flags/categories.go b/internal/flags/categories.go
index da1889b48..b15ff4ffd 100644
--- a/internal/flags/categories.go
+++ b/internal/flags/categories.go
@@ -36,6 +36,7 @@ const (
 	MetricsCategory    = "METRICS AND STATS"
 	MiscCategory       = "MISC"
 	BuilderCategory    = "BUILDER"
+	ValidationCategory = "VALIDATION"
 	TestingCategory    = "TESTING"
 	DeprecatedCategory = "ALIASED (deprecated)"
 )
diff --git a/validation/api.go b/validation/api.go
new file mode 100644
index 000000000..6d59b6bc4
--- /dev/null
+++ b/validation/api.go
@@ -0,0 +1,113 @@
+package validation
+
+import (
+	"compress/gzip"
+	"encoding/json"
+	"io"
+	"net/http"
+	"time"
+
+	validation "github.com/ethereum/go-ethereum/eth/block-validation"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/gorilla/mux"
+)
+
+type BlockValidationApi struct {
+	validationApi *validation.BlockValidationAPI
+}
+
+func (api *BlockValidationApi) getRouter() http.Handler {
+	r := mux.NewRouter()
+
+	r.HandleFunc("/", api.handleRoot).Methods(http.MethodGet)
+	r.HandleFunc("/validate/block_submission", api.handleBuilderSubmission).Methods(http.MethodPost)
+	return r
+}
+
+func (api *BlockValidationApi) handleRoot(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte("OK"))
+}
+
+func (api *BlockValidationApi) handleBuilderSubmission(w http.ResponseWriter, req *http.Request) {
+	var prevTime, nextTime time.Time
+	receivedAt := time.Now().UTC()
+	prevTime = receivedAt
+
+	var err error
+	var r io.Reader = req.Body
+
+	isGzip := req.Header.Get("Content-Encoding") == "gzip"
+	if isGzip {
+		r, err = gzip.NewReader(req.Body)
+		if err != nil {
+			log.Error("could not create gzip reader", "err", err)
+			api.RespondError(w, http.StatusBadRequest, err.Error())
+			return
+		}
+	}
+
+	limitReader := io.LimitReader(r, 10*1024*1024) // 10 MB
+	requestPayloadBytes, err := io.ReadAll(limitReader)
+	if err != nil {
+		log.Error("could not read payload", "err", err)
+		api.RespondError(w, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	nextTime = time.Now().UTC()
+	readTime := uint64(nextTime.Sub(prevTime).Microseconds())
+	prevTime = nextTime
+
+	payload := new(BuilderBlockValidationRequestV3)
+
+	// Check for SSZ encoding
+	contentType := req.Header.Get("Content-Type")
+	if contentType == "application/octet-stream" {
+		if err = payload.UnmarshalSSZ(requestPayloadBytes); err != nil {
+			log.Error("could not decode payload - SSZ", "err", err)
+		}
+	} else {
+		if err := json.Unmarshal(requestPayloadBytes, payload); err != nil {
+			log.Error("could not decode payload - JSON", "err", err)
+			api.RespondError(w, http.StatusBadRequest, err.Error())
+			return
+		}
+	}
+
+	nextTime = time.Now().UTC()
+	decodeTime := uint64(nextTime.Sub(prevTime).Microseconds())
+	prevTime = nextTime
+
+	// Validate the payload
+	err = api.validationApi.ValidateBuilderSubmissionV3(&validation.BuilderBlockValidationRequestV3{
+		SubmitBlockRequest:    payload.SubmitBlockRequest,
+		ParentBeaconBlockRoot: payload.ParentBeaconBlockRoot,
+		RegisteredGasLimit:    payload.RegisteredGasLimit,
+	})
+	validationTime := uint64(time.Now().UTC().Sub(prevTime).Microseconds())
+
+	l := log.New("isGzip", isGzip, "payloadBytes", len(requestPayloadBytes), "contentType", contentType,
+		"numBlobs", len(payload.BlobsBundle.Blobs), "numTx", len(payload.ExecutionPayload.Transactions),
+		"slot", payload.Message.Slot, "readTime", readTime, "decodeTime", decodeTime, "validationTime", validationTime)
+
+	if err != nil {
+		l.Info("Validation failed", "err", err)
+		api.RespondError(w, http.StatusBadRequest, err.Error())
+		return
+	}
+	l.Info("Validation successful")
+	w.WriteHeader(http.StatusOK)
+}
+
+func (api *BlockValidationApi) RespondError(w http.ResponseWriter, code int, message string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(code)
+
+	// write the json response
+	response := HTTPErrorResp{code, message}
+	if err := json.NewEncoder(w).Encode(response); err != nil {
+		log.Error("Couldn't write response", "error", err, "response", response)
+		http.Error(w, "", http.StatusInternalServerError)
+	}
+}
diff --git a/validation/api_test.go b/validation/api_test.go
new file mode 100644
index 000000000..5d6d160f3
--- /dev/null
+++ b/validation/api_test.go
@@ -0,0 +1,290 @@
+package validation
+
+import (
+	"bytes"
+	"errors"
+	"math/big"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"time"
+
+	builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb"
+	builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
+	"github.com/attestantio/go-eth2-client/spec/bellatrix"
+	"github.com/attestantio/go-eth2-client/spec/capella"
+	"github.com/attestantio/go-eth2-client/spec/deneb"
+	"github.com/attestantio/go-eth2-client/spec/phase0"
+	"github.com/ethereum/go-ethereum/beacon/engine"
+	"github.com/ethereum/go-ethereum/common"
+	beaconConsensus "github.com/ethereum/go-ethereum/consensus/beacon"
+	"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/eth"
+	blockvalidation "github.com/ethereum/go-ethereum/eth/block-validation"
+	"github.com/ethereum/go-ethereum/eth/downloader"
+	"github.com/ethereum/go-ethereum/eth/ethconfig"
+	"github.com/ethereum/go-ethereum/miner"
+	"github.com/ethereum/go-ethereum/node"
+	"github.com/ethereum/go-ethereum/p2p"
+	"github.com/ethereum/go-ethereum/params"
+	"github.com/holiman/uint256"
+	"github.com/stretchr/testify/require"
+)
+
+var (
+	// testKey is a private key to use for funding a tester account.
+	testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+
+	// testAddr is the Ethereum address of the tester account.
+	testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
+
+	testValidatorKey, _ = crypto.HexToECDSA("28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0")
+	testValidatorAddr   = crypto.PubkeyToAddress(testValidatorKey.PublicKey)
+
+	testBuilderKeyHex = "0bfbbbc68fefd990e61ba645efb84e0a62e94d5fff02c9b1da8eb45fea32b4e0"
+	testBuilderKey, _ = crypto.HexToECDSA(testBuilderKeyHex)
+	testBuilderAddr   = crypto.PubkeyToAddress(testBuilderKey.PublicKey)
+
+	testBalance = big.NewInt(2e18)
+)
+
+func generateMergeChain(n int) (*core.Genesis, []*types.Block) {
+	config := *params.AllEthashProtocolChanges
+
+	config.TerminalTotalDifficulty = common.Big0
+	config.TerminalTotalDifficultyPassed = true
+	engine := beaconConsensus.NewFaker()
+
+	genesis := &core.Genesis{
+		Config: &config,
+		Alloc: types.GenesisAlloc{
+			testAddr:                         {Balance: testBalance},
+			params.BeaconRootsStorageAddress: {Balance: common.Big0, Code: common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604457602036146024575f5ffd5b620180005f350680545f35146037575f5ffd5b6201800001545f5260205ff35b6201800042064281555f359062018000015500")},
+		},
+		ExtraData:  []byte("test genesis"),
+		Timestamp:  9000,
+		BaseFee:    big.NewInt(params.InitialBaseFee),
+		Difficulty: big.NewInt(0),
+	}
+	testNonce := uint64(0)
+	generate := func(_ int, g *core.BlockGen) {
+		g.OffsetTime(5)
+		g.SetExtra([]byte("test"))
+		tx, _ := types.SignTx(types.NewTransaction(testNonce, common.HexToAddress("0x9a9070028361F7AAbeB3f2F2Dc07F82C4a98A02a"), big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(&config), testKey)
+		g.AddTx(tx)
+		testNonce++
+	}
+	_, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, n, generate)
+
+	return genesis, blocks
+}
+
+// startEthService creates a full node instance for testing.
+func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) {
+	t.Helper()
+
+	n, err := node.New(&node.Config{
+		P2P: p2p.Config{
+			ListenAddr:  "0.0.0.0:0",
+			NoDiscovery: true,
+			MaxPeers:    25,
+		},
+	})
+	if err != nil {
+		t.Fatal("can't create node:", err)
+	}
+
+	ethcfg := &ethconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256}
+	ethservice, err := eth.New(n, ethcfg)
+	if err != nil {
+		t.Fatal("can't create eth service:", err)
+	}
+	if err := n.Start(); err != nil {
+		t.Fatal("can't start node:", err)
+	}
+	if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil {
+		n.Close()
+		t.Fatal("can't import test blocks:", err)
+	}
+	time.Sleep(500 * time.Millisecond) // give txpool enough time to consume head event
+
+	ethservice.SetEtherbase(testAddr)
+	ethservice.SetSynced()
+	return n, ethservice
+}
+
+func assembleBlock(eth *eth.Ethereum, parentHash common.Hash, params *engine.PayloadAttributes) (*engine.ExecutableData, error) {
+	args := &miner.BuildPayloadArgs{
+		Parent:       parentHash,
+		Timestamp:    params.Timestamp,
+		FeeRecipient: params.SuggestedFeeRecipient,
+		GasLimit:     params.GasLimit,
+		Random:       params.Random,
+		Withdrawals:  params.Withdrawals,
+		BeaconRoot:   params.BeaconRoot,
+	}
+
+	payload, err := eth.Miner().BuildPayload(args)
+	if err != nil {
+		return nil, err
+	}
+
+	if payload := payload.ResolveFull(); payload != nil {
+		return payload.ExecutionPayload, nil
+	}
+
+	return nil, errors.New("payload did not resolve")
+}
+
+type testBackend struct {
+	api *BlockValidationApi
+}
+
+// newTestBackend creates a new backend, initializes mock relays, registers them and return the instance
+func newTestBackend(t *testing.T, ethservice *eth.Ethereum) *testBackend {
+	t.Helper()
+
+	api := &BlockValidationApi{
+		validationApi: blockvalidation.NewBlockValidationAPI(ethservice, nil, true, true),
+	}
+
+	backend := testBackend{api}
+	return &backend
+}
+
+func (be *testBackend) request(t *testing.T, method, path string, payload []byte) *httptest.ResponseRecorder {
+	t.Helper()
+	var req *http.Request
+	var err error
+
+	if payload == nil {
+		req, err = http.NewRequest(method, path, bytes.NewReader(nil))
+	} else {
+		req, err = http.NewRequest(method, path, bytes.NewReader(payload))
+		req.Header.Add("Content-Type", "application/octet-stream")
+	}
+
+	require.NoError(t, err)
+	rr := httptest.NewRecorder()
+	be.api.getRouter().ServeHTTP(rr, req)
+	return rr
+}
+
+func TestBlockValidation(t *testing.T) {
+	genesis, blocks := generateMergeChain(10)
+
+	// Set cancun time to last block + 5 seconds
+	time := blocks[len(blocks)-1].Time() + 5
+	genesis.Config.ShanghaiTime = &time
+	genesis.Config.CancunTime = &time
+	os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex)
+
+	n, ethservice := startEthService(t, genesis, blocks)
+	defer n.Close()
+
+	backend := newTestBackend(t, ethservice)
+
+	parent := ethservice.BlockChain().CurrentHeader()
+	statedb, err := ethservice.BlockChain().StateAt(parent.Root)
+	require.NoError(t, err)
+	nonce := statedb.GetNonce(testAddr)
+	ethservice.APIBackend.Miner().SetEtherbase(testBuilderAddr)
+
+	tx1, err := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
+	require.NoError(t, err)
+	ethservice.TxPool().Add([]*types.Transaction{tx1}, true, true, false)
+
+	cc, err := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
+	require.NoError(t, err)
+	ethservice.TxPool().Add([]*types.Transaction{cc}, true, true, false)
+
+	baseFee := eip1559.CalcBaseFee(params.AllEthashProtocolChanges, parent)
+	tx2, err := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
+	require.NoError(t, err)
+	ethservice.TxPool().Add([]*types.Transaction{tx2}, true, true, false)
+
+	execData, err := assembleBlock(ethservice, parent.Hash(), &engine.PayloadAttributes{
+		Timestamp:             parent.Time + 12,
+		SuggestedFeeRecipient: testValidatorAddr,
+		Withdrawals:           []*types.Withdrawal{},
+		BeaconRoot:            &common.Hash{42},
+	})
+	require.NoError(t, err)
+	payload, err := ExecutableDataToExecutionPayloadV3(execData)
+	require.NoError(t, err)
+	blockRequest := &BuilderBlockValidationRequestV3{
+		SubmitBlockRequest: builderApiDeneb.SubmitBlockRequest{
+			Signature: phase0.BLSSignature{},
+			Message: &builderApiV1.BidTrace{
+				ParentHash:           phase0.Hash32(execData.ParentHash),
+				BlockHash:            phase0.Hash32(execData.BlockHash),
+				ProposerFeeRecipient: bellatrix.ExecutionAddress(testValidatorAddr),
+				GasLimit:             execData.GasLimit,
+				GasUsed:              execData.GasUsed,
+				// This value is actual profit + 1, validation should fail
+				Value: uint256.NewInt(125851807635001),
+			},
+			ExecutionPayload: payload,
+			BlobsBundle: &builderApiDeneb.BlobsBundle{
+				Commitments: make([]deneb.KZGCommitment, 0),
+				Proofs:      make([]deneb.KZGProof, 0),
+				Blobs:       make([]deneb.Blob, 0),
+			},
+		},
+		RegisteredGasLimit:    execData.GasLimit,
+		ParentBeaconBlockRoot: common.Hash{42},
+	}
+
+	payloadBytes, err := blockRequest.MarshalSSZ()
+	require.NoError(t, err)
+	rr := backend.request(t, http.MethodPost, "/validate/block_submission", payloadBytes)
+	require.Equal(t, `{"code":400,"message":"inaccurate payment 125851807635000, expected 125851807635001"}`+"\n", rr.Body.String())
+	require.Equal(t, http.StatusBadRequest, rr.Code)
+
+	blockRequest.Message.Value = uint256.NewInt(125851807635000)
+	payloadBytes, err = blockRequest.MarshalSSZ()
+	require.NoError(t, err)
+	rr = backend.request(t, http.MethodPost, "/validate/block_submission", payloadBytes)
+	require.Equal(t, http.StatusOK, rr.Code)
+}
+
+func ExecutableDataToExecutionPayloadV3(data *engine.ExecutableData) (*deneb.ExecutionPayload, error) {
+	transactionData := make([]bellatrix.Transaction, len(data.Transactions))
+	for i, tx := range data.Transactions {
+		transactionData[i] = bellatrix.Transaction(tx)
+	}
+
+	withdrawalData := make([]*capella.Withdrawal, len(data.Withdrawals))
+	for i, withdrawal := range data.Withdrawals {
+		withdrawalData[i] = &capella.Withdrawal{
+			Index:          capella.WithdrawalIndex(withdrawal.Index),
+			ValidatorIndex: phase0.ValidatorIndex(withdrawal.Validator),
+			Address:        bellatrix.ExecutionAddress(withdrawal.Address),
+			Amount:         phase0.Gwei(withdrawal.Amount),
+		}
+	}
+
+	return &deneb.ExecutionPayload{
+		ParentHash:    [32]byte(data.ParentHash),
+		FeeRecipient:  [20]byte(data.FeeRecipient),
+		StateRoot:     [32]byte(data.StateRoot),
+		ReceiptsRoot:  [32]byte(data.ReceiptsRoot),
+		LogsBloom:     types.BytesToBloom(data.LogsBloom),
+		PrevRandao:    [32]byte(data.Random),
+		BlockNumber:   data.Number,
+		GasLimit:      data.GasLimit,
+		GasUsed:       data.GasUsed,
+		Timestamp:     data.Timestamp,
+		ExtraData:     data.ExtraData,
+		BaseFeePerGas: uint256.MustFromBig(data.BaseFeePerGas),
+		BlockHash:     [32]byte(data.BlockHash),
+		Transactions:  transactionData,
+		Withdrawals:   withdrawalData,
+		BlobGasUsed:   *data.BlobGasUsed,
+		ExcessBlobGas: *data.ExcessBlobGas,
+	}, nil
+}
diff --git a/validation/config.go b/validation/config.go
new file mode 100644
index 000000000..146c964a0
--- /dev/null
+++ b/validation/config.go
@@ -0,0 +1,18 @@
+package validation
+
+type Config struct {
+	Enabled            bool   `toml:",omitempty"`
+	ListenAddr         string `toml:",omitempty"`
+	Blocklist          string `toml:",omitempty"`
+	UseCoinbaseDiff    bool   `toml:",omitempty"`
+	ExcludeWithdrawals bool   `toml:",omitempty"`
+}
+
+// DefaultConfig is the default config for validation api.
+var DefaultConfig = Config{
+	Enabled:            false,
+	ListenAddr:         ":28546",
+	Blocklist:          "",
+	UseCoinbaseDiff:    false,
+	ExcludeWithdrawals: false,
+}
diff --git a/validation/service.go b/validation/service.go
new file mode 100644
index 000000000..06f848309
--- /dev/null
+++ b/validation/service.go
@@ -0,0 +1,75 @@
+package validation
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/ethereum/go-ethereum/eth"
+	validation "github.com/ethereum/go-ethereum/eth/block-validation"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/node"
+	"github.com/ethereum/go-ethereum/rpc"
+)
+
+type Service struct {
+	srv *http.Server
+}
+
+func NewService(addr string, validationApi *validation.BlockValidationAPI) *Service {
+	api := BlockValidationApi{
+		validationApi: validationApi,
+	}
+	s := &Service{
+		srv: &http.Server{
+			Addr:    addr,
+			Handler: api.getRouter(),
+		},
+	}
+	return s
+}
+
+func (s *Service) Start() error {
+	if s.srv != nil {
+		log.Info("Service started")
+		go s.srv.ListenAndServe()
+	}
+	return nil
+}
+
+func (s *Service) Stop() error {
+	if s.srv != nil {
+		s.srv.Close()
+	}
+	return nil
+}
+
+func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error {
+	if !cfg.Enabled {
+		return nil
+	}
+
+	var err error
+	var accessVerifier *validation.AccessVerifier
+	if cfg.Blocklist != "" {
+		accessVerifier, err = validation.NewAccessVerifierFromFile(cfg.Blocklist)
+		if err != nil {
+			return fmt.Errorf("failed to load validation blocklist %w", err)
+		}
+	}
+	validationApi := validation.NewBlockValidationAPI(backend, accessVerifier, cfg.UseCoinbaseDiff, cfg.ExcludeWithdrawals)
+	validationService := NewService(cfg.ListenAddr, validationApi)
+
+	stack.RegisterAPIs([]rpc.API{
+		{
+			Namespace:     "validation",
+			Version:       "1.0",
+			Service:       validationService,
+			Public:        true,
+			Authenticated: true,
+		},
+	})
+
+	stack.RegisterLifecycle(validationService)
+
+	return nil
+}
diff --git a/validation/types.go b/validation/types.go
new file mode 100644
index 000000000..d1cf63665
--- /dev/null
+++ b/validation/types.go
@@ -0,0 +1,62 @@
+package validation
+
+import (
+	"encoding/json"
+
+	builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb"
+	builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
+	"github.com/attestantio/go-eth2-client/spec/deneb"
+	"github.com/ethereum/go-ethereum/common"
+)
+
+type HTTPErrorResp struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+}
+
+type BuilderBlockValidationRequestV3 struct {
+	builderApiDeneb.SubmitBlockRequest
+	ParentBeaconBlockRoot common.Hash `json:"parent_beacon_block_root" ssz-size:"32"`
+	RegisteredGasLimit    uint64      `json:"registered_gas_limit,string"`
+}
+
+func (r *BuilderBlockValidationRequestV3) MarshalJSON() ([]byte, error) {
+	type denebBuilderBlockValidationRequestJSON struct {
+		Message               *builderApiV1.BidTrace       `json:"message"`
+		ExecutionPayload      *deneb.ExecutionPayload      `json:"execution_payload"`
+		BlobsBundle           *builderApiDeneb.BlobsBundle `json:"blobs_bundle"`
+		Signature             string                       `json:"signature"`
+		RegisteredGasLimit    uint64                       `json:"registered_gas_limit,string"`
+		ParentBeaconBlockRoot string                       `json:"parent_beacon_block_root"`
+	}
+
+	return json.Marshal(&denebBuilderBlockValidationRequestJSON{
+		Message:               r.Message,
+		ExecutionPayload:      r.ExecutionPayload,
+		BlobsBundle:           r.BlobsBundle,
+		Signature:             r.Signature.String(),
+		RegisteredGasLimit:    r.RegisteredGasLimit,
+		ParentBeaconBlockRoot: r.ParentBeaconBlockRoot.String(),
+	})
+}
+
+func (r *BuilderBlockValidationRequestV3) UnmarshalJSON(data []byte) error {
+	params := &struct {
+		ParentBeaconBlockRoot common.Hash `json:"parent_beacon_block_root"`
+		RegisteredGasLimit    uint64      `json:"registered_gas_limit,string"`
+	}{}
+	err := json.Unmarshal(data, params)
+	if err != nil {
+		return err
+	}
+	r.RegisteredGasLimit = params.RegisteredGasLimit
+	r.ParentBeaconBlockRoot = params.ParentBeaconBlockRoot
+
+	blockRequest := new(builderApiDeneb.SubmitBlockRequest)
+	err = json.Unmarshal(data, &blockRequest)
+	if err != nil {
+		return err
+	}
+	r.SubmitBlockRequest = *blockRequest
+	return nil
+}
diff --git a/validation/validation_request_ssz.go b/validation/validation_request_ssz.go
new file mode 100644
index 000000000..34b649b6a
--- /dev/null
+++ b/validation/validation_request_ssz.go
@@ -0,0 +1,195 @@
+// Code generated by fastssz. DO NOT EDIT.
+// Hash: cb7006db7808ee76b3c3bcadc1a063f3beffa6f9f14fcc7a3d6f91c2cd700fb1
+// Version: 0.1.3
+package validation
+
+import (
+	builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb"
+	builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
+	"github.com/attestantio/go-eth2-client/spec/deneb"
+	ssz "github.com/ferranbt/fastssz"
+)
+
+// MarshalSSZ ssz marshals the BuilderBlockValidationRequestV3 object
+func (b *BuilderBlockValidationRequestV3) MarshalSSZ() ([]byte, error) {
+	return ssz.MarshalSSZ(b)
+}
+
+// MarshalSSZTo ssz marshals the BuilderBlockValidationRequestV3 object to a target array
+func (b *BuilderBlockValidationRequestV3) MarshalSSZTo(buf []byte) (dst []byte, err error) {
+	dst = buf
+	offset := int(380)
+
+	// Field (0) 'Message'
+	if b.Message == nil {
+		b.Message = new(builderApiV1.BidTrace)
+	}
+	if dst, err = b.Message.MarshalSSZTo(dst); err != nil {
+		return
+	}
+
+	// Offset (1) 'ExecutionPayload'
+	dst = ssz.WriteOffset(dst, offset)
+	if b.ExecutionPayload == nil {
+		b.ExecutionPayload = new(deneb.ExecutionPayload)
+	}
+	offset += b.ExecutionPayload.SizeSSZ()
+
+	// Offset (2) 'BlobsBundle'
+	dst = ssz.WriteOffset(dst, offset)
+	if b.BlobsBundle == nil {
+		b.BlobsBundle = new(builderApiDeneb.BlobsBundle)
+	}
+	offset += b.BlobsBundle.SizeSSZ()
+
+	// Field (3) 'Signature'
+	dst = append(dst, b.Signature[:]...)
+
+	// Field (4) 'ParentBeaconBlockRoot'
+	dst = append(dst, b.ParentBeaconBlockRoot[:]...)
+
+	// Field (5) 'RegisteredGasLimit'
+	dst = ssz.MarshalUint64(dst, b.RegisteredGasLimit)
+
+	// Field (1) 'ExecutionPayload'
+	if dst, err = b.ExecutionPayload.MarshalSSZTo(dst); err != nil {
+		return
+	}
+
+	// Field (2) 'BlobsBundle'
+	if dst, err = b.BlobsBundle.MarshalSSZTo(dst); err != nil {
+		return
+	}
+
+	return
+}
+
+// UnmarshalSSZ ssz unmarshals the BuilderBlockValidationRequestV3 object
+func (b *BuilderBlockValidationRequestV3) UnmarshalSSZ(buf []byte) error {
+	var err error
+	size := uint64(len(buf))
+	if size < 380 {
+		return ssz.ErrSize
+	}
+
+	tail := buf
+	var o1, o2 uint64
+
+	// Field (0) 'Message'
+	if b.Message == nil {
+		b.Message = new(builderApiV1.BidTrace)
+	}
+	if err = b.Message.UnmarshalSSZ(buf[0:236]); err != nil {
+		return err
+	}
+
+	// Offset (1) 'ExecutionPayload'
+	if o1 = ssz.ReadOffset(buf[236:240]); o1 > size {
+		return ssz.ErrOffset
+	}
+
+	if o1 < 380 {
+		return ssz.ErrInvalidVariableOffset
+	}
+
+	// Offset (2) 'BlobsBundle'
+	if o2 = ssz.ReadOffset(buf[240:244]); o2 > size || o1 > o2 {
+		return ssz.ErrOffset
+	}
+
+	// Field (3) 'Signature'
+	copy(b.Signature[:], buf[244:340])
+
+	// Field (4) 'ParentBeaconBlockRoot'
+	copy(b.ParentBeaconBlockRoot[:], buf[340:372])
+
+	// Field (5) 'RegisteredGasLimit'
+	b.RegisteredGasLimit = ssz.UnmarshallUint64(buf[372:380])
+
+	// Field (1) 'ExecutionPayload'
+	{
+		buf = tail[o1:o2]
+		if b.ExecutionPayload == nil {
+			b.ExecutionPayload = new(deneb.ExecutionPayload)
+		}
+		if err = b.ExecutionPayload.UnmarshalSSZ(buf); err != nil {
+			return err
+		}
+	}
+
+	// Field (2) 'BlobsBundle'
+	{
+		buf = tail[o2:]
+		if b.BlobsBundle == nil {
+			b.BlobsBundle = new(builderApiDeneb.BlobsBundle)
+		}
+		if err = b.BlobsBundle.UnmarshalSSZ(buf); err != nil {
+			return err
+		}
+	}
+	return err
+}
+
+// SizeSSZ returns the ssz encoded size in bytes for the BuilderBlockValidationRequestV3 object
+func (b *BuilderBlockValidationRequestV3) SizeSSZ() (size int) {
+	size = 380
+
+	// Field (1) 'ExecutionPayload'
+	if b.ExecutionPayload == nil {
+		b.ExecutionPayload = new(deneb.ExecutionPayload)
+	}
+	size += b.ExecutionPayload.SizeSSZ()
+
+	// Field (2) 'BlobsBundle'
+	if b.BlobsBundle == nil {
+		b.BlobsBundle = new(builderApiDeneb.BlobsBundle)
+	}
+	size += b.BlobsBundle.SizeSSZ()
+
+	return
+}
+
+// HashTreeRoot ssz hashes the BuilderBlockValidationRequestV3 object
+func (b *BuilderBlockValidationRequestV3) HashTreeRoot() ([32]byte, error) {
+	return ssz.HashWithDefaultHasher(b)
+}
+
+// HashTreeRootWith ssz hashes the BuilderBlockValidationRequestV3 object with a hasher
+func (b *BuilderBlockValidationRequestV3) HashTreeRootWith(hh ssz.HashWalker) (err error) {
+	indx := hh.Index()
+
+	// Field (0) 'Message'
+	if b.Message == nil {
+		b.Message = new(builderApiV1.BidTrace)
+	}
+	if err = b.Message.HashTreeRootWith(hh); err != nil {
+		return
+	}
+
+	// Field (1) 'ExecutionPayload'
+	if err = b.ExecutionPayload.HashTreeRootWith(hh); err != nil {
+		return
+	}
+
+	// Field (2) 'BlobsBundle'
+	if err = b.BlobsBundle.HashTreeRootWith(hh); err != nil {
+		return
+	}
+
+	// Field (3) 'Signature'
+	hh.PutBytes(b.Signature[:])
+
+	// Field (4) 'ParentBeaconBlockRoot'
+	hh.PutBytes(b.ParentBeaconBlockRoot[:])
+
+	// Field (5) 'RegisteredGasLimit'
+	hh.PutUint64(b.RegisteredGasLimit)
+
+	hh.Merkleize(indx)
+	return
+}
+
+// GetTree ssz hashes the BuilderBlockValidationRequestV3 object
+func (b *BuilderBlockValidationRequestV3) GetTree() (*ssz.Node, error) {
+	return ssz.ProofTree(b)
+}