diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go
index e0915880bdfe..20d3e22681a4 100644
--- a/op-chain-ops/genesis/config.go
+++ b/op-chain-ops/genesis/config.go
@@ -20,6 +20,8 @@ import (
 	"github.com/ethereum-optimism/optimism/op-chain-ops/state"
 	"github.com/ethereum-optimism/optimism/op-node/eth"
 	"github.com/ethereum-optimism/optimism/op-node/rollup"
+
+	"github.com/ethereum-optimism/optimism/op-service/feature"
 )
 
 var (
@@ -430,7 +432,7 @@ func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.Storage
 	if block.Number() == nil {
 		return storage, errors.New("block number not set")
 	}
-	if block.BaseFee() == nil {
+	if feature.CustomizeL1BaseFeeByTransactions(block.BaseFee(), block.Transactions()) == nil {
 		return storage, errors.New("block base fee not set")
 	}
 
@@ -446,7 +448,7 @@ func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.Storage
 	storage["L1Block"] = state.StorageValues{
 		"number":         block.Number(),
 		"timestamp":      block.Time(),
-		"basefee":        block.BaseFee(),
+		"basefee":        feature.CustomizeL1BaseFeeByTransactions(block.BaseFee(), block.Transactions()),
 		"hash":           block.Hash(),
 		"sequenceNumber": 0,
 		"batcherHash":    config.BatchSenderAddress.Hash(),
diff --git a/op-node/rollup/derive/attributes.go b/op-node/rollup/derive/attributes.go
index 9383c642c901..52661208a35d 100644
--- a/op-node/rollup/derive/attributes.go
+++ b/op-node/rollup/derive/attributes.go
@@ -11,6 +11,8 @@ import (
 	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
 	"github.com/ethereum-optimism/optimism/op-node/eth"
 	"github.com/ethereum-optimism/optimism/op-node/rollup"
+
+	"github.com/ethereum-optimism/optimism/op-service/feature"
 )
 
 // L1ReceiptsFetcher fetches L1 header info and receipts for the payload attributes derivation (the info tx and deposits)
@@ -100,6 +102,14 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
 			l2Parent, nextL2Time, eth.ToBlockID(l1Info), l1Info.Time()))
 	}
 
+	if feature.EnableCustomizeL1BlockBaseFee {
+		_, receipts, err := ba.l1.FetchReceipts(ctx, epoch.Hash)
+		if err != nil {
+			return nil, NewTemporaryError(fmt.Errorf("failed to fetch L1 block receipts: %w", err))
+		}
+		l1Info = feature.CustomizeL1BlockInfoByReceipts(l1Info, receipts)
+	}
+
 	l1InfoTx, err := L1InfoDepositBytes(seqNumber, l1Info, sysConfig, ba.cfg.IsRegolith(nextL2Time))
 	if err != nil {
 		return nil, NewCriticalError(fmt.Errorf("failed to create l1InfoTx: %w", err))
diff --git a/op-node/rollup/derive/l1_block_info.go b/op-node/rollup/derive/l1_block_info.go
index 0e4733d71069..13ae763015d0 100644
--- a/op-node/rollup/derive/l1_block_info.go
+++ b/op-node/rollup/derive/l1_block_info.go
@@ -54,11 +54,13 @@ func (info *L1BlockInfo) MarshalBinary() ([]byte, error) {
 	offset += 32
 	binary.BigEndian.PutUint64(data[offset+24:offset+32], info.Time)
 	offset += 32
-	// Ensure that the baseFee is not too large.
-	if info.BaseFee.BitLen() > 256 {
-		return nil, fmt.Errorf("base fee exceeds 256 bits: %d", info.BaseFee)
+	if info.BaseFee != nil {
+		// Ensure that the baseFee is not too large.
+		if info.BaseFee.BitLen() > 256 {
+			return nil, fmt.Errorf("base fee exceeds 256 bits: %d", info.BaseFee)
+		}
+		info.BaseFee.FillBytes(data[offset : offset+32])
 	}
-	info.BaseFee.FillBytes(data[offset : offset+32])
 	offset += 32
 	copy(data[offset:offset+32], info.BlockHash.Bytes())
 	offset += 32
diff --git a/op-node/rollup/driver/sequencer_test.go b/op-node/rollup/driver/sequencer_test.go
index 8e7db4269ff2..7ad517b07451 100644
--- a/op-node/rollup/driver/sequencer_test.go
+++ b/op-node/rollup/driver/sequencer_test.go
@@ -302,7 +302,7 @@ func TestSequencerChaosMonkey(t *testing.T) {
 		}
 	})
 
-	seq := NewSequencer(log, cfg, engControl, attrBuilder, originSelector, metrics.NoopMetrics)
+	seq := NewSequencer(log, cfg, engControl, nil, attrBuilder, originSelector, metrics.NoopMetrics)
 	seq.timeNow = clockFn
 
 	// try to build 1000 blocks, with 5x as many planning attempts, to handle errors and clock problems
diff --git a/op-node/sources/l1_client.go b/op-node/sources/l1_client.go
index d3d1d7438291..eec0a3ecfa28 100644
--- a/op-node/sources/l1_client.go
+++ b/op-node/sources/l1_client.go
@@ -14,6 +14,8 @@ import (
 	"github.com/ethereum-optimism/optimism/op-node/eth"
 	"github.com/ethereum-optimism/optimism/op-node/rollup"
 	"github.com/ethereum-optimism/optimism/op-node/sources/caching"
+
+	"github.com/ethereum-optimism/optimism/op-service/feature"
 )
 
 type L1ClientConfig struct {
@@ -75,7 +77,7 @@ func NewL1Client(client client.RPC, log log.Logger, metrics caching.Metrics, con
 // L1BlockRefByLabel returns the [eth.L1BlockRef] for the given block label.
 // Notice, we cannot cache a block reference by label because labels are not guaranteed to be unique.
 func (s *L1Client) L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error) {
-	info, err := s.InfoByLabel(ctx, label)
+	info, err := feature.CustomizeL1Label(ctx, s.EthClient, label)
 	if err != nil {
 		// Both geth and erigon like to serve non-standard errors for the safe and finalized heads, correct that.
 		// This happens when the chain just started and nothing is marked as safe/finalized yet.
diff --git a/op-proposer/proposer/l2_output_submitter.go b/op-proposer/proposer/l2_output_submitter.go
index bcf34e5a0969..d48d756d5cb9 100644
--- a/op-proposer/proposer/l2_output_submitter.go
+++ b/op-proposer/proposer/l2_output_submitter.go
@@ -26,6 +26,8 @@ import (
 	oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
 	oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
 	"github.com/ethereum-optimism/optimism/op-service/txmgr"
+
+	"github.com/ethereum-optimism/optimism/op-service/feature"
 )
 
 var supportedL2OutputVersion = eth.Bytes32{}
@@ -316,7 +318,7 @@ func proposeL2OutputTxData(abi *abi.ABI, output *eth.OutputResponse) ([]byte, er
 		"proposeL2Output",
 		output.OutputRoot,
 		new(big.Int).SetUint64(output.BlockRef.Number),
-		output.Status.CurrentL1.Hash,
+		feature.CustomizeProposeL1BlockHash(output.Status.CurrentL1.Hash),
 		new(big.Int).SetUint64(output.Status.CurrentL1.Number))
 }
 
diff --git a/op-service/txmgr/txmgr.go b/op-service/txmgr/txmgr.go
index 37711506cbbf..8f8242f9288d 100644
--- a/op-service/txmgr/txmgr.go
+++ b/op-service/txmgr/txmgr.go
@@ -17,6 +17,7 @@ import (
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/log"
 
+	"github.com/ethereum-optimism/optimism/op-service/feature"
 	"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
 )
 
@@ -179,13 +180,13 @@ func (m *SimpleTxManager) craftTx(ctx context.Context, candidate TxCandidate) (*
 		rawTx.Gas = candidate.GasLimit
 	} else {
 		// Calculate the intrinsic gas for the transaction
-		gas, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{
+		gas, err := m.backend.EstimateGas(ctx, feature.CustomizeCraftL1CallMsg(ethereum.CallMsg{
 			From:      m.cfg.From,
 			To:        candidate.To,
 			GasFeeCap: gasFeeCap,
 			GasTipCap: gasTipCap,
 			Data:      rawTx.Data,
-		})
+		}))
 		if err != nil {
 			return nil, fmt.Errorf("failed to estimate gas: %w", err)
 		}
@@ -194,7 +195,7 @@ func (m *SimpleTxManager) craftTx(ctx context.Context, candidate TxCandidate) (*
 
 	ctx, cancel = context.WithTimeout(ctx, m.cfg.NetworkTimeout)
 	defer cancel()
-	return m.cfg.Signer(ctx, m.cfg.From, types.NewTx(rawTx))
+	return m.cfg.Signer(ctx, m.cfg.From, types.NewTx(feature.CustomizeCraftL1Transaction(rawTx)))
 }
 
 // send submits the same transaction several times with increasing gas prices as necessary.
@@ -392,7 +393,7 @@ func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transa
 		return tx
 	}
 
-	rawTx := &types.DynamicFeeTx{
+	rawTx := feature.CustomizeCraftL1Transaction(&types.DynamicFeeTx{
 		ChainID:    tx.ChainId(),
 		Nonce:      tx.Nonce(),
 		GasTipCap:  gasTipCap,
@@ -402,7 +403,7 @@ func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transa
 		Value:      tx.Value(),
 		Data:       tx.Data(),
 		AccessList: tx.AccessList(),
-	}
+	})
 	ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout)
 	defer cancel()
 	newTx, err := m.cfg.Signer(ctx, m.cfg.From, types.NewTx(rawTx))
@@ -430,10 +431,10 @@ func (m *SimpleTxManager) suggestGasPriceCaps(ctx context.Context) (*big.Int, *b
 	if err != nil {
 		m.metr.RPCError()
 		return nil, nil, fmt.Errorf("failed to fetch the suggested basefee: %w", err)
-	} else if head.BaseFee == nil {
+	} else if feature.CustomizeSuggestedL1BaseFee(head.BaseFee) == nil {
 		return nil, nil, errors.New("txmgr does not support pre-london blocks that do not have a basefee")
 	}
-	return tip, head.BaseFee, nil
+	return tip, feature.CustomizeSuggestedL1BaseFee(head.BaseFee), nil
 }
 
 // calcThresholdValue returns x * priceBumpPercent / 100
diff --git a/packages/contracts-bedrock/contracts/L1/ResourceMetering.sol b/packages/contracts-bedrock/contracts/L1/ResourceMetering.sol
index 8a50079a662c..d5a5420a7bd2 100644
--- a/packages/contracts-bedrock/contracts/L1/ResourceMetering.sol
+++ b/packages/contracts-bedrock/contracts/L1/ResourceMetering.sol
@@ -152,7 +152,7 @@ abstract contract ResourceMetering is Initializable {
         // division by zero for L1s that don't support 1559 or to avoid excessive gas burns during
         // periods of extremely low L1 demand. One-day average gas fee hasn't dipped below 1 gwei
         // during any 1 day period in the last 5 years, so should be fine.
-        uint256 gasCost = resourceCost / Math.max(block.basefee, 1 gwei);
+        uint256 gasCost = resourceCost / 5 gwei;
 
         // Give the user a refund based on the amount of gas they used to do all of the work up to
         // this point. Since we're at the end of the modifier, this should be pretty accurate. Acts